From f2e8409181411ab228960fead449836c6a2d82cf Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Mon, 16 Sep 2024 00:43:31 -0700 Subject: [PATCH 01/32] AbfsBackoffMetrics new implementation --- .../hadoop/fs/azurebfs/AbfsCountersImpl.java | 10 +++ .../fs/azurebfs/InputStreamStatistics.java | 75 ++++++++++++++++++ .../AbstractAbfsStatisticsStore.java | 59 ++++++++++++++ .../src/test/resources/readVectored.plantuml | 28 +++++++ .../src/test/resources/readVectored.png | Bin 0 -> 211653 bytes 5 files changed, 172 insertions(+) create mode 100644 hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/InputStreamStatistics.java create mode 100644 hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsStore.java create mode 100644 hadoop-tools/hadoop-azure/src/test/resources/readVectored.plantuml create mode 100644 hadoop-tools/hadoop-azure/src/test/resources/readVectored.png diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java index c4d3e05cdb25d..39661b085981c 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java @@ -101,6 +101,8 @@ public class AbfsCountersImpl implements AbfsCounters { private AbfsBackoffMetrics abfsBackoffMetrics = null; + private InputStreamStatistics inputStreamStatistics = null; + private AbfsReadFooterMetrics abfsReadFooterMetrics = null; private AtomicLong lastExecutionTime = null; @@ -170,6 +172,10 @@ public void initializeMetrics(MetricFormat metricFormat) { switch (metricFormat) { case INTERNAL_BACKOFF_METRIC_FORMAT: abfsBackoffMetrics = new AbfsBackoffMetrics(); + inputStreamStatistics = new InputStreamStatistics(); + inputStreamStatistics.increment("TEST1"); + inputStreamStatistics.increment("RETRY_ONE", "TEST3", 2); + System.out.println(inputStreamStatistics.toString()); break; case INTERNAL_FOOTER_METRIC_FORMAT: abfsReadFooterMetrics = new AbfsReadFooterMetrics(); @@ -253,6 +259,10 @@ public AbfsBackoffMetrics getAbfsBackoffMetrics() { return abfsBackoffMetrics != null ? abfsBackoffMetrics : null; } + public InputStreamStatistics getInputStreamStatistics() { + return inputStreamStatistics; + } + @Override public AtomicLong getLastExecutionTime() { return lastExecutionTime; diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/InputStreamStatistics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/InputStreamStatistics.java new file mode 100644 index 0000000000000..d9e658c02aa6b --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/InputStreamStatistics.java @@ -0,0 +1,75 @@ +package org.apache.hadoop.fs.azurebfs; + +import org.apache.hadoop.fs.azurebfs.statistics.AbstractAbfsStatisticsStore; +import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.iostatisticsStore; + +public class InputStreamStatistics extends AbstractAbfsStatisticsStore { + public InputStreamStatistics() { + IOStatisticsStore st = iostatisticsStore() + .withCounters("TEST1", "TEST2") + .build(); + setIOStatistics(st); + + IOStatisticsStore retrySt = iostatisticsStore() + .withCounters("TEST3", "TEST4") + .build(); + setIOStatistics("RETRY_ONE", retrySt); + } + + public InputStreamStatistics(Map> map) { + for(Map.Entry> entry : map.entrySet()) { + IOStatisticsStore st = iostatisticsStore() + .withCounters(entry.getValue().toArray(new String[0])) + .build(); + setIOStatistics(entry.getKey(), st); + } + } + + public IOStatisticsStore localIOStatistics() { + return InputStreamStatistics.super.getIOStatistics(); + } + + public IOStatisticsStore localIOStatistics(String key) { + return InputStreamStatistics.super.getIOStatistics(key); + } + + public Map localIOStatisticsMap() { + return InputStreamStatistics.super.getIOStatisticsMap(); + } + + public long increment(String name) { + return increment(name, 1); + } + + public long increment(String name, long value) { + return incCounter(name, value); + } + + public long increment(String key, String name) { + return incCounter(key, name); + } + + public long increment(String key, String name, long value) { + return incCounter(key, name, value); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + + String result = localIOStatisticsMap().keySet().stream() + .map(key -> "\"" + key + "\": " + statisticsToString(key)) + .collect(Collectors.joining(", ")); + + sb.append(result); + sb.append('}'); + return sb.toString(); + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsStore.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsStore.java new file mode 100644 index 0000000000000..2c658abfeb563 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsStore.java @@ -0,0 +1,59 @@ +package org.apache.hadoop.fs.azurebfs.statistics; + +import org.apache.hadoop.fs.statistics.IOStatisticsSource; +import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public abstract class AbstractAbfsStatisticsStore implements IOStatisticsSource { + private static final String BASE_KEY = "BASE"; + private final Map ioStatisticsMap = new ConcurrentHashMap<>(); + + protected AbstractAbfsStatisticsStore() { + } + + @Override + public IOStatisticsStore getIOStatistics() { + return getIOStatistics(BASE_KEY); + } + + protected IOStatisticsStore getIOStatistics(String key) { + return ioStatisticsMap.getOrDefault(key, null); + } + + protected Map getIOStatisticsMap() { + return ioStatisticsMap; + } + + protected void setIOStatistics(final IOStatisticsStore statistics) { + setIOStatistics(BASE_KEY, statistics); + } + + protected void setIOStatistics(String key, final IOStatisticsStore statistics) { + ioStatisticsMap.put(key, statistics); + } + + protected long incCounter(String name, long value) { + return incCounter(BASE_KEY, name, value); + } + + protected long incCounter(String key, String name) { + return incCounter(key, name, 1); + } + + protected long incCounter(String key, String name, long value) { + return ioStatisticsMap.getOrDefault(key, null) == null ? 0L : ioStatisticsMap.get(key).incrementCounter(name, value); + } + + protected String statisticsToString(String key) { + IOStatisticsStore ioStatistics = ioStatisticsMap.getOrDefault(key, null); + + if (ioStatistics == null || ioStatistics.counters().isEmpty()) { return null; } + + return ioStatistics.counters().entrySet().stream() + .map(entry -> "\"" + entry.getKey() + "\": " + entry.getValue()) + .collect(Collectors.joining(", ", "{", "}")); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/resources/readVectored.plantuml b/hadoop-tools/hadoop-azure/src/test/resources/readVectored.plantuml new file mode 100644 index 0000000000000..57d17d545cfc6 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/resources/readVectored.plantuml @@ -0,0 +1,28 @@ +@startuml +'https://plantuml.com/sequence-diagram + +autonumber +Developer -> AbfsInputStream: readVectored(List ranges, IntFunction allocate) +AbfsInputStream -> VectoredReadUtils: getFinalRanges(ranges) +VectoredReadUtils -> AbfsInputStream : List getFinalRanges +loop range in finalRanges: +AbfsInputStream -> VectoredReadThreadUtils : getThread() +VectoredReadThreadUtils -> AbfsInputStream : thread +AbfsInputStream -> thread : invoke(range) +thread -> AbfsInputStream : control back to main thread +par threadExec: +loop buffer:multipleBuffersInRange +thread -> BufferManager : checkIfAlreadyQueued() +alt bufferIsNotQueued: +thread -> AbfsInputStream : read(bufferRange) +AbfsInputStream -> AbfsClient : execute() +AbfsClient -> AbfsInputStream : result +end +end +end +end +AbfsInputStream -> VectoredReadUtils : convertToDevUnderstandableData() +VectoredReadUtils -> AbfsInputStream : List> result +AbfsInputStream -> Developer : result + +@enduml \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/test/resources/readVectored.png b/hadoop-tools/hadoop-azure/src/test/resources/readVectored.png new file mode 100644 index 0000000000000000000000000000000000000000..e121eea052b310740b5574a1b70661a53e6fbd29 GIT binary patch literal 211653 zcmeEuRaBI1`!5Cts30kgNOwyPQc}`Lhe&rz&46GapbR~PbV~_Hmx?fShqN+7mkc@N zzTy48cYohn|92nklYO%1#O2IlX71;?uj~5NJ)s(E3b*kn@o;c(ZYwFuY2o19d4q$4 zJ9gtTc&7gIC1LOno2R_Kr-h5FucMW1tZtm%E`P zkEhzX4lBE^UpC`4R>67(4EX!<1K9TlamerLIql@kDFWydyBmVsWM6N2nf;rQ3&K_b#zM3Uxe6oJyp zi+3F(o#UeFl>77lVD}391?27rKd7C#sYCyz(m?)RY|!oiX`_;JlUJXwN<>_So6MTk zPXj-lYadnhYDFF=Hcp#A6-ABhRk)T3p^M(ez4v}Zrq%IAa=tygaMp#EFJk3TZ~j^4 zu!B}&$>NXCRxGC0g%ZSe=(p9u9!`8SA4c`+ZipQ3ER_|R*?*E&z!Ip$JvbEKEcYJC zu`^jnj3CK%DDEdTlP>v2EWFY`ae95{VfHo#bMTgx>Bv z<}0@gNFLPm^-tYymZJF8wiZ{xJP!%4pGV(w#pZ}=Tov$qf*VhK>QltDJx^7p8apoc zIbl|~IgGDwbo=nn&ik6G_p5i-l0#>`W6&ENBfi&ga9-jl$w}+@n65SYCXmRF?wLHg za`L&0`Nq46m#pj$!g*`%XEGF~(+fKHyj#oUWDjQz#$#`P$q^4)D1xE= z&O$dztm`O1AP}-CF?Ac~Xz28IIe%H01p@n*ZWe#KJH}H&yzvKsL3ak#X?9ZN^RyUIVb>|5w zPIpTKch^D*Zv}tnzG>mV|M(vMqwH@}Y*(e7Y%I|kLyEMr7z(%U5zyeJ|h-M1?!$YJ0jR!q;j^ccbRqTI+P;R$ALfqE+~4(a81NR|v(g`q>eoUyA-BItPs4Cz@P*v@J$Y3Lb?yc8T(XM)b$0b#9(0NTqCc1} zExTjG>Xg|w`q{kY_!#PMboJuH=r41cHr%>@@2XXNev4MMJXoXP#Ts*ick$wR%dXWQCtVnETJiba&X<(`?W$90v{io`={GF)?!$lbvExRJ ze|=gj?8IByxI$?7EQy}=HlDqpbjX{*taL-i zA4;k4qwPMau}@DO`cktFX)jhmEhJ$dut#?D;*B4bT|2*4`C!u%s`9$Z5@wUe>fu~` zadfZ}ygaz>2!SHYBE*Jtdo=95vmMK7WP&QA6{GxpcTOExcEB~8um&}_+C zuyKj{>-&;85C%(Yyu~x;#2L7H<>6InLup^!&%A_JrRBLnF>&PoRZPL@f~@%iS82$U z;lcK0cR|H#D88Y+bMYRTH)VKlbZXpxk4vBQ?AKd*MGn^Uatx-=Z^wM*k707$PwR~BD`vcJ5T!c`y;}u zq_ximvx?F;AUGMgxi^Cc)6*Xld+M`F;(cm;OpQj#+$Z%Dx%d+An9sMZ>>FOPm$tz{ zRKfJc=?$0l@7)MaXCQy#7fU*PUL9Y-wxJ{$whg|6pQ9jM`STs=r4go6MWnoNokw3C7?(GpD~19A?bBtf{NvP4}|@GAK_w z4UMduVw|zGxFgRWfCiZni6;rL4|vEC|KIE%(7j2tGD1LG0t3%B7<3MPof{UH-Z9^+ zxqo92uao2DdEK(Rf6G=d_^V!@5MRxamj1xJ^jX7?L?`@%NIUY44Ev7j`8(hcs{a2? z@&A5P3}wTPB2?2(DeX>=3B1^{|E@PBMVZeO0;Y?E3+N6yFS{U5bi3JXU?6VV2d}vw z1a*{Gt?Dk?gbcr{?D8<^R23=}0Vj7%DA~A-Yd-R{&NM1`*-fA!`%RV|?A+50zG>WC zj29Irr$Hj=77jbv_4cyM{sKGFgR+tFm=>tO>{mx$K`{hU$2y3|+7;G~-^S3x1!j5NZJ0w28QP`JM%KU8|&4W%K@NpkmLoV|3X{l*9x!@sNRHRIKz z(z8RJJLXNtm@nMB`7tbk&_Cb4jgOB**?)fxBCg~A@3p5(w~Xgdi0@H=wVU{_e*nwj zI@Cwxs&05}dQT0+sF*>wNKFhCFDfK7y~yZ{GOt8 zy93OaPbygj%5m)5LfFGq7jO-C&@p|Ov6{`M>t4%D2*F>2BZbvVgECsz5_cY1yNAUb zA30u>n+oLAi)X(%%t0Q!kA2f=7eWpOFsR5=Gykde?k+A})*YFmqY&uh#fb z)jf^)^_Rcf<4vU-WbH-D$OscGqi`5h7dIGB_M*P;Q`BU5b_O~tl-cJj?Rju6wg3K(TNJ@| z^9vCApac|SaMp3(C-h<|N+6ai`2<{~60>HDqrFmno4!-ovHi^ISyRSld4zS{9cqzp zQpGZF@al$hvlN<>g`Hj?Bc|QmDLZ;TnH&WJEH-2ku+^%!B(|ERnDzJrs*^xcPzNCs z@cO*bM^Aj^GI+l}G5tw}IMmUgNqxl96#-3dYz(`JTA|`-DK66lhSMS>uLMIvpIbBP(7S<%EbG;zP!jwA+=t8`68I&%=?fVSfRWl$?I03a#F*YRgfMn`k_uGWn(nm%xb7 zJuNo;Fq9WKN;=5HQybl?4(_5UxQhsG0eAFIm*$f^h(rZRG<0fxve|EEfJIR|uXi}- zZZ}fW7lZutv~a7Gmhbudl3)2W;!W}I&&DfeM@hrvOfh|7iCVo#A-n0riOV_9L$uqG zsueQP@Z_}S2-U-5EU5h`^dpjDC34@gkW(A?u*4~tFwbAO4VuKf4>gXd^WrgX@X3M5 zzKo8oS54kq(%SSIWKK3iq@=(OKIarBt_&?gwh7TF>o8DS&=>m2VdI@q1&IQSn!HR> zJyCe1?EPkADZOCRB-)rhQ}>!}vZ(uFI}h_>D>z42EHG@1sOasp1LUe)PjYppG$Q=T zM3qzEpI>aS)FrUWP&>O-?6%N*Vctcvc6SqC> z>_I=UvaIH{>WpnX#&C%AJOBLbrmJhmR)^`7T54%5dMQ(I^Bn&%Rt6pY2&Ma^BMQIG zPw~P;dUb6)aJWDlK|sO#ETKe$5fjZ}aEwl!jIxD%Y_swx4)+1g_wY}*mLkS&n)Dmg zcrfi$@Lix+qu;KibpZSx-&=wL!^(*QZK-8Ec?KS=P3sfdZ8tVcUangmd%QT)hYaJ( zT#uHaQ;q)c(PICMtX}a_cD;q3b{eaQ)OO?uo)V;_J`zslQTadI2?09998I!^`+z(` zkxjG8aZiEHsh{Ph9FayWkC`dLuQWz&d9f%p@YKpgm837Vh%mf4?BnVKx`)~T0nSyl zOz3p$M(86WV6$Zw4xBBnS=$;Ta~RfcUOd*zLokzoD=5VygI`FePgt9 zj|~v15?It=PT@A@8m9dl8ZFqlV|SSgjUxNsK=9z!H%I&NaxYE@$`y=f5!`IARR=F2 zuCoeur^%{Cf$rq}RBlz(cBBa?)HNgBJheu!KgMZ|q`X%0=DWsjQ3toi6O7@KZpPCf^wwqqbv9X?r&!1sX_?G+? z1{LvjYV-^|@6O~vPxLI<7h8@!@X|9L`0^yNyzGFg)k`SKQeD_(ChQEEHF<@!_CHF6 zG;pW!X`wLGI`i>|ZRGpJ8mCEwHBA^)uSj3Yvi)6WT%l{F1+wWO2R+>wUJ000482HF z8WGoW*wG#17~7$RYdk~!y2oz%cy*(tvw&^iHDS->-uWGpR^FR>5>oQazAOHU@peVu zixsVb2$~Scp>4}T&(TuXs#Y>yEL>$azWGYxHGYb^;F3PGgj*q4Y;Q`)atvrR%D>(A z0K}1%3LN>=vp&|V>TuJ2^Zm7uLsh^jcE+U%>jJysFqA7vO)2O!@xG7OZ^!2* zQqj)|?sZ`{8pEeNCKv zf-<}Da-n8e(CbaYv$^mScr`wVm!@f!DV zvSP0SRY)=4a(h&BEyCA)pqy zk1s_a@h7t67i-$S;G4Un!_~8=F{j)#&SJc}_ZLxqE*735f#voi)-ZQCw#KISIOTLM zFU^?&(SeGs2?y*=tBN<VU)-1HPkT;{(fbLL}S~nQ6X#iGL{$_iVxO8YjaQHldY;>&{_uS zNpIAHz?{`IOwqeIGJi#jO9n5kx&rv7xdqs{mB0NPYcL57F)Am)yC?;h?v`k0%H2d_ zpLF!OCBDp&j$i#LJU&lghn|;`&WN|WUt5k43)P{gW7C|uL!rM%&6*xV&{|sm`|S&& z&3gR_hZ5|Ar>rJ*-2f`O&1XBqZ@oj)Cewz88@C0MXu*$25oP-D!ygTlJwCz{f*0OUb% z6RpWZc6Y&4&dovai`p}LCCn|HEe$n;wuFb(8V=l z1hWi48$Z2Rckb!!jZYGj$bOfa=@Vw^D}FuKvLl-0vU(za3ry5$djSUUy2JFLA)d+n zPyJ&hXFU1Z-MaQs-NkML*O8}sT}rG?8^g#mP-@Y6MJ39Okl>*?bT?cp8sgcUuja+o!nT<&PU>E&28!D=?l-kIqj}u~1wt!OSkAON+j$k5)CvQDo zL~)xoXsIQ85^uzAE(3%IDO0*wDW(wZikWe#$(Y8> zI~46Kpw7{a1zeJCK$mmI$8WFO%K)Rk{Znq>;RlH4EZaSVMQDpZdl=bSk))*GFd{Y3K5|tdS@6L!q(cp7KW(3~!tW zfF3FAi&1$vJN0dWdhjGuZL5uHb5-`s@qwc$$%R;#ZJi_s(BfF>?EMky#?=s7%FdkO zEH5^P>AH1eC*3LF?E& z-NgV@Nywd)9^Rg;SqAoiT6KtcQNdib`xnP?$^zNcJj4$Nj#Pgq6~28`c)8Xd)@iHt zIvvz=z4Bqw_^aVJ9o`Jd*5ds~E71z)8lT!bj9c-+16=+}g<@^J&vQzJf{H(HRo|44 zCfQ%PLg(}Aeo22`AFNJCE+yVyzCbI-5pGK@c}U~?XYshqBV(&&An0>+&rQQEB(CM* zU=*l6ad&j-;iJ_lx0I%rw?cM^yL$~vj79f1PF4zC63ycleupTqOjPSTa<^^^UQY2h z){IcP(DraF$w0#ySRRtves8X(CVTq&BUa37rFAY!W$-$(wMEMHm)v6SwCcVot3f$B zfsfefx?kGU@#eFdTFlBjC!$dc$Ob?|Y%imeghh}#T_2sY&v;DgZk-K12@GvzF(qzf zlDLhnDfhJRlKR82$&qdi*;pDtIeD1!3AHotWp#UynBY|4d~C<``B|LdVonkGJ+F>wku$h5;1mOMR`L=zsTIS`AGLI>IiRSrhA= zuNWqt8sLXFR-}>fJX4bESl?pwqv&RPQ*lHBaq^4@lL0Y{T3p#vS+%aYR*qf0=N(Bu z0LiQOTz1>YXnt0x{wh2%J|m-ye!%L>9Cui+qVv)n~gv<1DAb!g3cmD=G)B_PWGLVl+f#f)L*yS7Vx z_Qvh%f|vR;D$08$y;h!>e8ZG+KVVQO1Y}@6Bqf~Fo{aUZJ1p-=d)T6MiwfNXwjtv{ zSP=y-Alv;=T6o#6mDH+Sk$KDGaX9uvs-#Er!E>pzW{cUaXn4KfF1fo5V!a+ZGnAb{ z4U^r!KBJa$xEycfTunz7;!JTHYS)FFuHMv-Vb;S)IraaEwN0+`j8Mg_5=plZn{dO@ z3-nBVhsY-J1VSvCSm|u!{YTo^d z#c)#W&Ugdsg+ZXyBN!^NfZ6~uiH-+UpH&B2698otaIsmRa3Z3@3&prCt6&7w(EcL5 zlU=keXrG!z6m!bQ(x-)0D1UkV1^v%ZY?9nodN`cq-7LRV=X<&8bgQmWlI5BMuA=9O zN9c-{u(0_~&B>~D>^7j!xu2glvs#(O&LbBQ2f@&LN(rO=Le;HRGkORK_H*CkdmLPa zQp_`3@Eri{gQOeH&CNRP9d@yRLe*`(zS##xT_n<#O?Ql{GaA{KW0_`yq#kBXP+w#a5~lVCuw+ZATfxC0-(Jz>#0-75 zhu95#dP;><2(vOS)tZ->xt1Uk3t+uF+b%~UKC6+ixKv;<?D+lSs(M!8sRUj%FJ4NKyJv8u3Jq?{59l!jIRSC(P5f zauUZ=2Sxz^8+mncRm16|5|5!_-|dG*(o`&(fA|S>UukUi$_G=ZMn+e6*~n#Gv1|kk z+9+DsPAG81JU9~#A?c&_W#-3b><3yS3;(%1A+~bHrBObrMH8tIjES*0ig>IU{3$*4NkaTH}P?WHc4^-ZgyTjYo^T)?t?oWLm=pWwf zlYE%Mt(Yu4w!K(p=6G5h(cz1PpFLxK@x^}Rt#K^5X~J&5tgJ64v6pR=$g6u@QH<_% zz4|PcY9$)Jx}ADRXI||r$?9*@@3iu>Y!&R@0j2!Am^y1UwH7k20Jpm5Z(`u9jUQl3 zV*4A>aFsh($nT!dT@T4tOCbN}x zeSWcd+GZS=t^3V}Uc0~fjBxy^4w(@CPQV@z&V-GGraKjAeLr$ufjz`b&Tn6*^-&Du z>w0%NzrwZZ`B;f})7dGWeGntwqi!bBv{gCA!x49pM11#(Jhc=hy0D<=`h}m~4QNyn zv-h`Prn<{ri;`C2kEIehvA@2-2g}r@B;S-guCV5s|1$iC8u3Nn`i=vDjU7KUH>n`U zd=P;XySi-S6{hQg^H5{FO9z$gf-fSya(H+Fz~1IYqHctkMfH}2ira-A%(Qr-mh|yj zL|x-;-#x~g(fg^1PtJ|FglNK%M_QgOs|-?zjL(CCQiHBM=tJ((e#rfJCF=XFR|=%X zN<5L0v3v9#H2_R!;iPc~*A6)i8bk^Hlsz;t{tc8~&$XW))`Z6i%NZ@h+pD97w2}vq z$1fU&w0zGjz=rlTK`nt^^r9faFHThPCdbLo)8~mvbW8;c!M@@u!8ls7^F* zcj}2S)nJZ_Ua|+q8d5SZ<`Q_gGyAi+_-Wx@qClW`A2Mf&hpZ33Zf}CBV<=Ze*_kM% z0nmWCTm1=hBDGEw7T>H8LK(kO?quLZLf>DxuGa4=00=&ieiraTp^Rq%T^~j9Enfr& zpd|NqveJ0n=13^Jd`j^sMU0Qh3J(?_*tO4@=a?q&%*{?ErV{IY zpRBHA>TO~a7VMdF`R3miYTEsTcbR@Ke)SA1h0ToHTM!C06dt?WHRQJM}ecBcDbfXVB7;NS$-HLV(fl zB+<&4^^`w?k3Kw8&mbsEQDYDxZ&XDc8JjiJxH)2@Uuo}QI`arQ>%KCqj!|85VDyWt zo!XCPQb`SLg6*eZx20lh7icB@kaN-}Kt}2j<7wax=lDVNB*&NJ0uA*AB_leYmu$W} z1Nz7F+ym8MVch2*tTq!1OX?=9SquPcd>ldTaO4UK5IRhE=92<>y*q9r&g8q31 z|A6AeF7Z)`+1VU13pF#Tm1=O!Ql<*%3XjLkffZQoUeyHK-Z9(+f~VChIwXy(6`c?z z#Jb0?g;UILN1XPZmAN{6LAM3^5J$iu;TvMndeVpTv$d+rgJ$yTQ1&5 zR#{?A5u*NkbkTJwDJds|Dy1v9u_s4xIIZMqIM3cBr1@8{7&P6&!n$LSx%LhEC{cdc zDd*CUfN{08k6Q6uc&>rB-}HGgl!uhRod9#kXm5LM+$DxB-g*9~ck}|P1Oy17+v2z{ z>muflQlmE-_ti;KmJ_Qx0+?m_u04O^n#L%?EH(b;F{Z1upS0+}d3-P~Kf>ur zRB``&@M;6u8McO#XTa88l(3)&058QSb>#l61ALXHb;ZLJJzDlM(O*}rbh9*p|7uEEJrS+**Xyx6 zY;UVcH5G}%`yQ{4qmBtBx;}cjLpzqYP*Uy0a9`gmp1Au4q#ZB2yF8K+Upg!x&M9JE zG->;io=cp_WG0pPVID4i8!F%;=Mu22Pxj8BQg3IWEaHrcguhuz}$#6q;c@>bp7K$J4d;Eb?JR&0$ z&7@Zj_DZcly`u|s)O*s!53b}@n&{SVH~eVC4?%U-IoJR ztQWd{jpYK0OvT*v29E^8c zjNeMZ;(V9L9*qH-0pHs2-r&2t7A5s1#aHpm4(y{KuSkDLs(#v^HT#BS?)(yI94Y53 zdTff{rgFy?%_!zUgblP7HSgvC>D21rE-y%2-n3TBq z@*3j*ETVFSTYhQY=UY}<&#;n1=BOYBM9bs1V)X!c0E9`2i3%<597Xl6Z$bVd2N144 zWVwKfMm+aWV^;ep(+l4j1%lvo^DLI&4I|xHGu(1vJ(ASjlUJ_l9I}1=I43^&=DbA9 zYP!mNw;>$BimiXKA<<31Gzc5O29MTSd;>UY1uy=0x!9rNhVn{v`y?aKI}2b$9I3+W z6jppJ%{liL&j)?c^ICUHi5zXQ0g^C4Re;v;I;=^k&rkc`<;Ms!4qfOfdQ9 z5saHA5N;4kt6eRDEG$;oH20*{BmEsl9p~-+^lnn7iDYvc2^&pX4Zb3{vtY4Vla+_9 z-7zgcJ!BmS=V|&ra?ZDk+pW|zX0AQW>GTbyEf#_PjD{5hQOt=$2CtebvgbK+n(QhsdMnoJEn*~|AD&Mf zEpIN$sTvzF#m>sW&wBInDPDi10hTP9`7O5>Lo4sg`vGA<=SN!n`H%gvF7hv z>ze5+l{V@FjG<%EGS6juSbBK$f-l zw*lipR5?wWBFcaKes5(B;r3NM7BzADSAA9UmM1ATwz!#VnYK@UHn%Xb#|2u1NsR)$ zem7`CIVq62*S9PF=JqG4L)$B3_eYB-ojHWEaDd{uD0X+(dpwb8fP=atVx-vr{+QdE zcy#seH|JRko6o^Dd8V;3L%d|_)B~b{?h4BBoOaHgR`&ZFE@;i$`J)4Rar-PcAj|K_vp9;R?6}Kl^ye)L%V}*H|=EeKlzafJK1~v{FxRAN5hTeGE1Np z+7mP`0l1b2X*V0p3HnC~oGWxpierwg?t)fUY>To}*yMZQU#3H5_nF2Wx zI<-7L=+JTk4p~E@L!qwI0kIUeCjKs6&uJdn2arP8WMThXRJ$$KGiW%rRSfNb?W!hzh}?hK3+xh1m!2LKV}C z2%UL1GOoS9>OoXP)A}27>*Xvt;KjbW&S<|>7b|c`qwc=YJ?k)fygSLQ|M>@J^z{SJ z=z2Cp#+a2_2GLumX}`!-AWl2oVUH$+ZZ-xEDkQq| z-G1&t!)1;4UlEK}GuLjTQc=h|A23@3%(YJr$?Ln7LPIvoC0EKUW>wyPh=`lAc{&60 zraX@Kt?M08mtQ|0GlBMSpZ zMN2!ASV29l$H!}HjtCnG%Z)EO1zLWuObuf00YV5XQhZ-ao8_b&+r~~sQ(IJ*BD(Eu zYIRW&i_j^M%y{zULHU{giaeM&UCEfZ2bsAz4!z%6{J4j%UHf(K8FgCmQny@u+ocM%^Jw*aPO%8Df~C1|kD7@ep?lY< z81d4-%F)?^vWODY!R*m^WEn)y1C|S}=Ah8SArVK6ue#(XllsYn(xoGAs_0sq^3Ir> z8=`zgvD=2V0;`89JS8WlhD+O^4@9aw}onw%_){Nno;&94h14VK|A*x zZmvkJZTTx(Sf-`~5w}l}NG6`9ezU%#6)mj`_50qo8&C5w9wZyszukR3`^rAA=blV< z0B@txtIE!!u~^YGUAA%JkxGjtuz?Gapl{cQyFiBhxu#FxZk#Wt^U zTfyU13j<3&s~D3Z#}4P~GXum$&pjSGO=O*U{&!7KWPDh^s}QvYMIG#!$45=BH0Tko zc5Dukj+*Z>b>QW_H)%Rk@ zAMd|m04Gfx#~qC{CxBr)Ssf_kHa84Jw64{(C-$R7mC)K2?q*ZnM@)3pfoJ#Ybau9o zX(y-q4FkaagV7EG1`q&bt>IhTwb?<~Hoh3J_m{75p_*K#{WK7UTg4RfjDGPinW{(n z1a>ZjtJCYw+gKy>%O9;MBiD0wikJn@O(Fot>Io3Nj= zunw}pt}o@xd>#uDphlw08Z4+HtXOE*ePafD3sy|+%yRvz z%czMnZUjpQKrM<>t=4f~#SfUMwMiDUQ4O~-hCTbz1aznRpg-3vEhLNAWSbZ^Z5Av| zx}4Rz{&uGnXJsdGNFPYPH0!%qTAg)HU%q5!$SSxC@rb4a{#~~gsVb#GnGlf8=wuP| z?($b4>`?>Bkm!BYME(an zF6riJDyzo=x*!F0a-`!}#7D+VG3@l|>u2%7Lcf2xevoq@T z9>Of(&3~5L!%fMPJOhw@LyM98o(RLMt3zjAAUF$1Nkc@QUTM?9;>f3`i+zo-E26iI{BttR(rHZGZgp`_dkJJ#d~iD5j7kA$F3P^(&S(S zL52pJVAo58&A)q8F#w9haT>4HXm13pND#sG01!W-ly^G;zFGsLS4ou`rFz-nn01u< znZH!}C%@g`N4UcZP@sUB=?)aA@?E`S3DSqoQ&xq`R=7IAS9o2gBj~*$gEgYfxR%u? zVAXlcm$V*##{=w3w^?GQC=H|N7)HPHv+cfpciga~zsV6@4Hs(H;NdJZBIOt)WXfH4 zPmefuf^-Q-_1~@mqPeGctFE->2X>}PyI z@UUCpkFev_51$T?8I&vsvTEnW=+tDjfJyRWZjy${@BI(FxzutkT63#aEk zMtn!Vn*t9oZu@JZ6Q6&V-jtL9*+-DG*|SMYdGYJ35=Co z*n7-3?_ttrWzrHpwcQ5TCs8=h^!a7jTr0-rn4eQWbf~Cfj-12qUKKeXi!bC@Mm%rgxK^;FT==CAdyA zK2aLHk850&`Q?MEE;PCC(v`OzL*m@F+SpN}=4%5Wq_{gqteMUjDwLQ2cgPX^iwte- zMaBt=RWp9n?qF zz#jcwLZBId5~ALAvqGo_ggr?uT%JBMEkLEhRZunEyfr8h>Ms3XwBPFw6a$XQ#>Eid z1y2-I-3IlZ{8za+el*KdlY~`WW)lL3deY41@Dan(zADjo*iVYM$1gw|&@h2B$HLb` zASE%u$!^xv0Co1SVveFtYpVf{{DB0jO*NXJAXma?zk`LU=P+4#{db?mR`K$#*+7!y zk<%odS0ZujY|iY2h5hMqtFdacB=V@H*B?#{{COQntdp?cPL0tSnZojVZbK22T=DNT zJBSFj-$(THu`+Lt3+SojvVfLMKqFSD{+xpL^z_7eHrr!y!&uek4`a&HvN?~dAo+YQ zAGPO zSYS!P>}O5~qN*pV$!M2K$8p0mdxddb4a)$L0mJfjg5`lJWnsDi3>ps5J0lsL8&Qt` zVRK%5^W7I9HDWABE1&rmnR(PJWP)*PEDA|HqRJ7V8w)GNe^2$WW6SA^RhRX73w2I} zuor-c*5xu@G~FnOsRqU-&V2?^!RUaz?O*bcF{^CJhqkT-5MQm@#k53+hklx)!|hQP zwMj4Fk_*%aW-OGv&+gG}G*Q$Z5)qN@OqJlWZny z-b<`5VMXiSN~&5L+>3M?TJYm`@1u%uL;Y1L`$rqm4$)O*p`QEG`+t<4Z~e|vLC_8p z*~Ui6qDw)&baJ@3Yi0^Rh<^J!8t#9zZn6_3+#!H-^-Z636v9@plKO_OG)Qc*Gcr$4 z)0Rg+M#fhg+}#iOJxd!M4+Mu8J;7h5fhVe-n@b#t3LS5C4_U7NXDRu1Agwy&%20Qv z?sp(}Hy>-FZ^~2P}#Z=@xr${2hyeCu9^Tk+uv_sy(ptu6=)E2>K211wg#}~ z#lx!xMiq*w3Lw8q155Wl!S;DzC4kJwMyJL2m2QPau4nOhhl)?46X*c}W5Kv8J3$Os zATatw0YJB#eWJXV$a~$<_pxPrVLRwHXFPW1yIW{OW7@g zS*vZ~RYb}M=XrZz>{pWdCO$QX9j!}&^eGf@z3S;vrFf%+-z{_k4!JCJdg{1I(FI1| zg=ZxHeF*&)V1O~cw(6a0j@A6qrt!(-ClOc8XxGunM_{e626h2 zT^kKrNfJjtYpIHAhgyjr%QGGaqWChpP$$u$dNj9)^8_Q2w+wtCJ_P%RNk!KUH-)?% z6Q~Z0_kr6AZm z4wd+HN~!C&pyv`KE`-+vk@c63f;M5dfSC zj5!-ixUM2_Cw?2NZeCsETl@-O8T#t1uZ@cdyJ3Yjhn`2bkWvy{#o~H%TcdHUyPtwm ze4oEPg-f#I&(B-Hg8$<{!2`ho1uu4o%gF zCZRrwxw7@|$ibFfQo|w&WZ)?EI1I|O*X?d|2x4naVcCx`ZXK3eizKP8`iPjJ_M%_W z7fPyPR78;hQMULUBrP&n4x*(I5Z*h%KVM!N`1ekavUDfzim%09Q;JLCS(#m|ZfGTM zNA}PIQCi&fSIb4-y52j`f=tNHos=BQmy!*Rtbo`bbz0bCxl@g~K*@TV+46_sD%!Mu zCJR@AL>V1!>p6Tqj1~w{C6813bs159-Me?MdQj!@H2vgB>ryVJ8zP+G`|F73%kJ+s zH<(o@Y5C~11(|4@W}JI;)YKl7?14njcvwTDw zc|dpu9O_6bd|IU6q030{D9MOwf>CAcVyhElqkn)zD}Oj(?ta}!a2V$UMo-J=b&P{r zlzZm-zHaeEw-=E~eD*#KIL7@_rgiZvS2RhCP3SEyU;mucQ=Ql5;Os>SyzP^`K+3;>Z6 z#JDQX3))o+h`GmWN!X!HN2hz>rJM$9!$nkY0$)ftkeb~<*AgRP-Tkv7x{7bx zRdV0i7aY~7^xGvH?_$~OaxyGhtpTS?=~?k*_ce8kmfa29N{zJYcf$EqvBrp~_{V1S zb}IdUZF0=^*fLPhuf3->F^WQdymGcG2c&5b<*Rb?2m8FnRrKHcJPDjWDT!HXq=RfO zFAoLAu&^4tGg!F@r@)hw{iVo(ovKX78tqgiAVxQK_LFg7ym9|j2~lqQH-bo7kG%ma_o`D#sj*^*!=t9U{qdrK+~d zbmd?ts^}$vkegDy*EFJ&+TJJH>biCyNzc$gdm0au~ ziQdR=!{z?rxh??bimdbAY|T3l*O02~aGBIS;e?1TC4A>`ZajL`#qFL*+yF#l5O_LQt`KYJgFT}TAIHc>uH0lwG=`fMhFC1CoJZgU~!Ru#3m zN29u-jO0UBRWK0GG-esFQu3RP0zap~s&jm6Dz}>)xou5LBj&jfSmoedSi*yG`OrET zedY@6dQV*PMvyav=<;Njfi36$rl_b$?rqY};p(QlQ2ENCxQ{{1=l8K$`sF=?L~Q%j zSQ|u8s1A^wja9)2sK&aE@8+4ewE4vioSi4kI-v(fE?k2-MF5t?vuXwxE`uEJi<;8G zC~$+@g}~`258xA9YYqxq*Y-9VvCX5(eKnJPP zf{R`Uq@H1!M95)uFo$9J4nRkyfNSk57uig@Ce3fBZU9G&fW;$E#b7w9H3ED`gBay6 zsv;_bbN?z5qi{`;A{B%4b8xG++DfoU@<*YP{zMQF<+n+zKXgpPt)}>qs<)k@sT54w zbkR(ds$AEK17X_~ z;`^$-MJ%a4Vd2KtZj@<8K(q#pjd;A=Pq2!*kItc8;xkTK<;_X&ZceWbWU*-@GkNVA z*}xY&ffDU|*cwnHVQ=QmZz0M|O}S{b{{lcm=HVxuQC}KCm~XP~Nl@(HW1$3F6I9Io z?__7+N31`PMBU~ShORhaSfSlswEf{|9ZQ9#lfAU(v0g;HDzxL-`9hVFH;osr&YN5D z8->mkeDPY9F4Of}0%vE4CASkoM=QTN z*CIk*&d)POs27HZq{R$H1`C;d<2Oxx3P_I0ij8h7{|i3fbNj31%RGwN&wL+1J(a=r z^Y~@Xn8{dtI54@dz=}u%$#mj#cJ2Gm1zdpa>|l;Tf_8)9&`$}5HA%d`MNf}UkB(Na z7yu7N2_ULahe0d1z_iM&Yq##Tg@B})0iwJF4Dyl~TL!kjk&%GwBqK_()D(7p?4nSE z8$MeBHMH<>{?iwjTF6mj3xZV8XCUbV97sB``M6-eKcpRMS0n5wOh*}u4*YL1J#Cdl zazvp?LjZOJ8tLlT<}We6yn!i3|F`@woG$YtW!=|wtz`to93*aSeHhtB=K2Ib7dEsufkH@K(!xXYtAXx=~N z-I_UwdK}zJgO+RRJre@Qm{vs=l6*RlOq#b^M+~bx^|yfx{Lp0viL7&s6SL_B2Q$Rz z5WQc=kB2Fnu?x++zjY<3WUDqimY~LG7TV|Ou-|lT535Iksl%ewG@v^y(c;mkI+g=$ zhD*jQxfnyv2gYHdjcz}OL4bkj(U1YeG;?6soj$tL?Jq4WWmLC{=n@4qe;Pd6(8wcX1%$qRdY!mWP5bQ%MJ(JWjN%8g>2cWR+9eQuz26+W};iLa_F>{K?UAeD%S1Cc1}TM{+L2 zDnLuXsdSgRWwH{b{SFf3f%0QB}8HyC?<*il~5y z-~vGbr4(rvDJYGAg0xCWEjkt{C`xyCBT~{VQboGELFw+!Jr~dWeDC}1eZClH>@m(c zg4JykehGHMLvf6O3+LVTZi^Pcr|THu!pN+J<=cP zNoQha|5{ntVFx-eI%7ZobK{9}ou$@L(;2eS;I*!&ZYQQ!Nci$g;Q_+2NzcE=qw>aA zx#R}bHvj=jT=p|O_Iar8GX%`E+Q(vf;#u*zv^R~f%o`dSIt+HPd{KvRIQ`mML0G5$ z)kFQ=i|#RhS{lh_Zu*BOs64szI5rh>vBI13>WIa)hpEuNp|7VJSPYWrjVn$m(6|0; z%l$6i#$2~zZ_!vOYdtbTv(RtyPX#rA^ygoo&B{RRt>Et3Bn5iH^yql5+aQMTFfYrn zA(f5Ba^8xBLcKu0pgD4T7SH`|Hd)HGXQ8=wRkF+`ANW+IFDP6!A8`M^R!H+Bg45I^ zY7W{`+lYm+mG)K_=mtpCYP`3nVwU+<8j?9Q-vJ~TrUjl%CBm)Sdu6Fd&-$PVR5jkI|@W#(F;v!ds?_Mq{!&mCx!pZ6hfQBzSAQ>|%Se37S&$b78$KK&La6 zPXwIvk;b|v)NlI*!giH|{xwX#cP zjNAU)4E4R{f=#*FR9`Jzkjb?7fSNaFL&LPb)~M~b98*K9hA#bE%h`?(^uO?EK};~p zVl-gL#0Z7@W2^b0@(@kw6KRe(rt0aNNw+R&H#k5u#MEUCe!BnQHZUR80niL<>=+Zl ztiQXp%V@mnD#4c(+I`RW#0~&jEbpB^{6N50vOU5d&KtrlLseS%w%WFz)TL$t3LJ^F z^r6fePiUK3TCAK#)M7RbdzaV|!}a8}d!h|g0Tu#X{ruAY+ zwSS=X@<^loVB^tYZS&&HZCxfURa)kcj+c}=pt$^q9@}%?MDMN7JsEoBQTEMUdTnQ% z&|)E9uX731zEUIZiNhEF$rt#WUD$$3eXfkOY?xigIr^g}6fd7}DxceDjY2~!=&FdZ zUsGC-B3i5KdXlMJ;0uBmzRLZK25i^z>2#nNl3xE(5dKkjxV;@(iK=-vTVFfLXgJ?n z**8=;*A!mqdwbuDVT*?^P9{EFIMnuvEs)o17LE)*^xLqAy~A{zW>)TcCH)*!nI3|K z7z7gqyweUdZTZ6jWz3-dz)^%DA41)jDP6uVwIP~^wo8Ns>^Zrzog%l%yPoGD&Rh>c# zaQ(f3S?8}r#mSZe1yy(^Euzd_UDq}D3IZuIJ;BZD2RXa4M_nnluBrW|Kq_*6^G`?- z+r=J87B~!g0_X`H_NQj8&6kJmETB0KycoDzIy*!gY6CteZrcU+W<2sq#O&J_&tOIH zVUgUHDOta#W6rVs9C6s@RgFilD->ehmY7-?OH{Cl*M*X;>M`KE<~kLq`#6UkNQm-ZvjTt>g_XOoWd1fqDCL}{sQtx+faY9kJ)}i{!t^h zO7wyWG{9hjOjWyKPt~enK+>qcT0TYGU$VD&>zLQy{f*&){?3qcs_!d?`Y?7_*u7meWb$@tP#pr zg#S_8U8+XVc>6T37dLqnZ(jb=l-c83Sy-Q zCHF$FkCd+b^h_E6by9mbgXU|8-_D;6MX($N-`x0L@!}Q43!)bw8&ZWNNNyW^OFH&H zG)NOlzQM{RRO(Cv3>$$$h)XvLX$fDyg8NO$wAg1$#lv(-zTq}I9{n=~`-o}_5Km;X zL)RSy`Y_8c&*U=%y){^7TpDUOXV+CmfL6CxZLIpc*J*Ff)ZFY=0536{VPDuDf+RfO zcvNsuTRyvDFXXaw&W^<$wg!mu)-!Dw5WY<4m4!uUaus9hekQ9eO36R?ad;p#kER2-* zLWg`E=?o9Hwi@d!j0ixA-}KC;kk?oo!q|l}@y{-5Fo&0zP$Bz{}6Pr%_E zROU44uM=oM2NYl;ND&3EJ%?7zoxoQ}8?pRBr4A)xu8Y+SL_Y1|V!awp@))zpq`E@B zAV8vnsQJ6hvKV-28<{ho8-4O}cE*Z~BA!AAlSY<&yihf0>FuFa4_tM);B0a9Q~*ix zL@sC6Id3u!v7?5UifYZhAFI=YqkQSLbai{v6=M?(Jx(6K01zGI{~UMa)_f zRH)2dPfw1=aBmXgu2mj)+pvniVToQTnIC94xF4VA%0MlW0zL*C)1aa)1ScONLHY5C zU+?g?FhU82@qe>D_a>bOWm)oSgXm=6cBTxMM1;r~7*=z9W8JPvboM$&!W|_o@!8xg zb^MnOX#LmjC@Cd_AVlyJv3b#sh$o*;dJF426FulAeP_b@xffE`*#f+fP_~Qh*myol z=BbPB`0WUSmRXW;>a_9J-K{~VhTx1BX)O3Y^oowsnw>xgb5?mdrr1-|whw`DK@V~% z)jX&=a?^I4r21G}mfX?~R}ytr=nJx(FEk!0Gu~P#Lc6C!F(j91Da=;_$Q(@g3lh)c zS0&eARRr9V`*f=ca?}k{1_GldhoxcYOX2sezC8@;sCB*B+(;P?t}L^AFQ2Ome!HPl z^+}`m+Syv&CUU5*eL8qF%b8PbO6^;sS(`OnGOUfrB2lP|4oRfbpMe%|R-FDmcWKm9 zx~?hXvQ-`Kyzmwgr}kA(r5N)o!j3)e89P(=11bcZOu@4$%{WWB0NoUp%EjY-iz}2H z@Bv8rR|HZ0k;aa3^gEbT3ymfX!J2SK+j>@7_fJzrK7^*TD|gcf-O1wCm0z)Neb>^~UTt7@%`!9l)RID|@k%Taiy`Fx(q36G zZ#~re8$nR6j8# zT4Vjl?(?s&ypf2FFP`E{9(t4b&FmLe4WVBm_xX-YunW=LwXR1}$9HcSCl~gI0lyVO z5j(neLj&7>ruGl1qnHLCO706;Y1!MVF0c@xj5OdsQvE{K7MAJDrcLv;23ZzX@8Mf9 z#hOFSdU+nBxy>a=e`G%gE*DM10g`oe&nf+9(9g_OX$fj17un*!hw0B%iGY>_tGj?6 zKgeX@RPU_)^e>=-@$6u}UQwBa4PfFDmqfb4u_rUH2G>5#{=sG#k~!f~*6Q8hU^LU2 z3!*KAqs<0}HcuX!9m_R@Y){M%#dY6}lo20yK8R z@hz-|VJa#3qA`lYY~q!QcosFFB=W~odbL`r^hX~07zLV*K#Y(x^rhM$CD@v0A3~4& z?AGFK^{e*a3^Bzd&OYM^6~Fl=$A66%12+ql;C4i{XR8T3gC}_Oe^YvvUxHGiV>Y|kbA6f#?qvY~#q_9!u7~0ao^Ti3TlLP?ahF}Iv&k_;^uR1%hSy&*C#iRcCjtrT~Z@^`88@fcW)e! z#fye{VTL`HD20UCk#Z8Xvqw&~s)WGK-sL7!1T!*va>dYtp-xcEYpXQffSjp48`}|j z&?>xJoOF%4hLg!dmgmkqErzBVJWJL%*rAOl8Ak{?)n&YN)qwQ38jfQ&B76Ib`|xS`Tu|oi--mb$|P_ zwE%r6lrzq(5akQsPo|K#cmA?^=|8`A@i$&y=8zb5CCswbqaQHB3%G2Xu=Rkez67+#5{cX`865=bN7IZEi zjJ>uOBRMSRnxM(7r?$k6fnjq5igKk`y8}bvYLDPAv!iB>i@v;D6Id17lYFg@<5iY2 z=0q6j+Mw0?cHiopQ#S<&n~*b9QL$mbjFkj`k<^5t1)SLsUuMUc6Z?x}9`rpL0k zV3KxfR;a+hdJq~@=FJC$6)mAD6Pg`c13!T}5!|XEXZN8vzBggf3gYE?u~t5w#gs@? zyu=&np|Hm^up=TL{=tAIjRT5P$HZU7Ht&akj%B-%xz*I3ASex>KQDA;el>U+$V!fg9!T?-*c2)z?<4>4jNtdZ%b-;T$6Lxh2 z4~IixO$J_qGfX9NXfSMl$PPB(e5j`hsCLm%!MN%`SkhZYO>#)Uh5_*zYN%Kuw)q61 z6g})ZKY5zn*wH)dH`DPv3p96OrK)?KL)X73LHk-#)$P%z4N{S5;`xB=o+9ZMtN5_{^lstS;dSDT|AS`|Q2!Y7hReg6GPBakCEqEY^Ng zFw~{i`s6XK22Mg=O~UpX^_Z1?m4%9Q4%<}yxo_g1EtIYeNu`5TP}h3tr*zF1f)TK# z-2lfx$0?S^xluY3o11LifxQ5}NRq!CA^D^N;eFu~68(HmxelmaHKyXR$OtaR9P*&L zIH=A0YYJ3w^XCS)Kp=ccAvV2O{KXG@T4g+A;=nIJ7I!WifrlG72Ju0MP4EpXfJlC$ zLb^2bQg;xsADQlBC>leLbMGP1k>t1JdD_oMJkY_&q*?a4Wsvs^k2Xwgsi zao;KpzjH>ze0F8JhTJdleiF9eh}PwM6(+FN$}*WCQ3S(UB3nMDpmW6UR9lJuP44D$ zHRH>KY2h`O>n8?k|CxaCy z5B^#!tVdWYJXfrB;J>>5Rtx@ZK_1ul9&Y}ly=q z)vvl?PKUT&6$G`gj1OC!waeySgQ9;5E^`jpKqR(>3ws zkL-G0k?3C=+k@@@jzf4Ll49HGXxeG6C$rRSa{7O-HT!jJhbYGqsRIG!mlamWgCw4% z;;NAwf2EynIBF$WJMpFy+j?w_6?tflC+H`2$`%(aygr;bdJEwetGoBdco7aW-*Q{z zxc+_&H=L8jqU7+&RidEZ5(44(UJW< z8xU3t<~AIyaJY8jZ;!oCI+N`8ErF4PBv>nXxgy;bc_^ zXi+*9TN{JtI*}FW8$;~GgM`c2i3bmm@a;XRGEDz|(!$PCCvUDG|_-i5hYt71ie2w$* z)?AEQDHV*DIDi=or`%Xv7l|2Ep590yW4MfiljQtU9Xn$oEPh|;3=@9o*5Ek-)6ZUr zaK11aoYSW&&ek!&kOk=l-Mq!1xNG3ve?6R1ZzgTxIK+ z1v`Bl9D(@LV94Kr{wtv2?A(pUy3nu4aH|+vh(7vH=7Zp@RMyopzQ7KqDBtLi*wC>1 zYmI{2yqog{?DPn_k=Geuqfhus1SbK2#?g5-qZuT&R0PKGgjtBm2 zMv3rcVK?|qfz@-Gyf`>v-yfX5irdzC3Ww6@`^4$z&)Wh zrCfdd|H~hn`R;ti-Q9hylkwzD{UYUq7wcF0sQ1i!Z`o^Z=Qk{H+(+g;pH^cIae&WE6H^Vt`J4TanvC=8f4{!~mBC+_bi)s6ad zaxu>+9FnN9j;Ko^r%oj$C*zZ{B!S=c-pZHH2wS=#ZqqE_oNSfAI9WG!20&d~0*zDz zr^r>itp$G$4>N}tyCtq7I7t8gFP9jt8uhkrJZ6HWc^shBD{;>oj3wsN(nSpR6Q-vq zb?U~cPCM*vwAVh>J$|4r!@K#S*m8^H{fg{VddNMHF%F@$Jn} z^G*S0*w4tbiQ78fP|d865f%jhoQMjqef;*sfw+4>p2sPlMUIi)d+fe*sPJ!%+zXmWRbm*C7gT{rX)-wiWqn%y3pvd!+T(Mx7Jh_ znPoZOgQ-@J?-E>U8UMs`q*eXn!+9Y08AE4$c$Q2W|9-qUIF$Q-V7;H5ltlF4h3N5d zi3Z#mW{~CzCwi#rGnNmMW3FomrhnYCY25eijdG=SV+cw~64t5=!RwXZKT!Q?%r=N3 zkb}qt@jD%X^=>ocI%pJHo%9ExX!?us(reI>!GMT?Ty0O4tN!5JDo*gbe0#*aF(Cz7 z#P<@MCxGLO|BQeF(3ipMZpJCgTADh%L90qNQI-3;ugz>wje#n3(@f?|G}-*e2c3+K z#1S0EF{|u*vSl`_Vu3Um#yp+*p3IVUy2G@W($S$|EZQDRPD90}sQ!!pd;sn5Po245 zwwx9a5HQ5@&)ct~nJ|@6pK{7NFN9Fk`z8M|_p7R{tu4qg3V=#kA07*H6P%(vk6S-Z zLrY7-sQkhMRl3NYDk|bv+pNwNS-aSuH~pLN+!gY^LL(KPuk<86Mi+k-k++WDV0hKG zp`-!N&{`#CNQi4&wRJ)P>PMQR&9t%ha)nU36_OCyda9@yQtQ2nc0W4YpN}D7R$mB- zzAAnjKAmb-du>%vJ;9(i3-yl#5qsHMglomL%(T9d{7#O#7}d4nIk56>3Z(TlWKhWH zgHOpUab!@=2Sq1zAo{>qha0uRms!qp(TO6qCqp%0@E_#a(*7Rn$SmWOmA3dN07|;$ z&~=qZdj_eeueQ2uM7z1N@I@k*W>aIALZTb~ap|M>>rzEcU90(a>{ z&XsUX(qAWz*apRaS9TPi{m!?3n~fRS{(zvMiG8QN?$zK(?p;U z?snexGB$3MzV0ikiVu}t@>lU|&(W;4P;BvE?#p??&ZJQ>H1aNR{$S9!5~K2%x->Xl z%lKkIZTUiOqm~_n(8)Je9xu+KhhQ*3J-Xg}?1olQNJvU(H9G2Yxi+ueR@WJ5s!!}!ZngB>TgvY-{wvHcjKd86-x67q`7gdU)zRyF}T*{=pL#HXu6Ck?e^X&4~>9~~B z-%f$Y{(@@XsME397WLqww_|<$Cp+}^(s25-BTz43v_pzUYz-ia`CI-msVAhX9p9jI z_yW=9(rDge0xdcx;<)HujJWMJEzz6@Izr3E4()RC@|LY;Ulc@+scsTHS%|rIsl)o9 z#dOWiFA(3seuT&op~&sFbeETRm~p*Bb8zCZsg@@HrdI6`(G2Gq|9An!YwL913rmU$ zb-U8gPuZ=gVR?zAG1KF4e><{UYw^zTMv6e5SA60|3SqSDsBQg1#=c=kl6%OlA0Iy4 zX&Js28_L2VD&2WsPDk=_(LOA_l1T_`UBfLgyedT(&<}+&2!-9I5e+-!jk`Z@^r-rI zqle%*3L{zOgNWX~;|bcn8}G~S=*q@XzScm$_h5~^_20#H0mSx;0Qen-_rq zhYO%?zv%PF{PSan+oP3R0|uKa+E{~Mgw5|$&2l@<*#4I%21nx*$KQ&0td@qtVLPV5 z{JRQR4G?(2F3M)iotCL&CV}wT;iz^#2(N8NZ9K;R%7~#j6VSX4#h#hBw|({>H=FCaAX+I7FD-6#OpPfloKMV;%M7Xj)0t!m({PXjDXeUrt!1p4rR~yewAF zwv3~OOJ0UP3h~r4E`nf|DVHqnV$I86^nD&qWt+LQ)Cts7bEM*S)Nh9?%zz%@$HBl~ zF(pN8>>O2Tt`+&$pr9a#zV%fZ=T{(CDP?of!RxLf;LjkQh$z)o*KpjOgfsvY4T=M; z?n4{%S*1(E)}x#FAHG7WB5%;}nDSV)*yImeuc~~Uz^UO^8Lu8;Quz;7J`6UoKsHe0 zO%`d8e)z&JWYkfgoZrzN(jPOMscYBGB4Zr)W+_dNnc>}T4H}2?X-8ZjeWMkHhF5A& z1~~%fkHP*)e>iiLp(W2mj|2|zh=U5p!@VrE(hx?K9`dAfZQIME%%4rCqpf=~l;rna z0u-RT_H|t8a?VpnbQA;!X0B{tJ$g}m(B4Sdu7zPa+}q4MzXBmgf+DmOOeI)`*V>*C zV4BNdFHLGqa%F2&R+Pt;MlIu#sJ6kp1oZk820xclbTntWcw=MZA|Xwkm!8kFB_CCD zS`N~^owEA}NK_z=RK8do&%N3-;;44!`9oH_X_{S;qPnoaKiUjtK4$rfs-~`_}*!tTtFzMrQm0#8pkCQ z0TLVAc`e)$o!&IQb_YAF5Zecoo~pzk(Fa*9r3SrBMfy*Hf56K((NR`bE~4tlUZuM#&ZluF-W~I~5H28>AVt{NrV{a}YmwnUwBYgG@7Im3 zzkO`yOIqJa{7$wCekFq3i_z)m;IU5Ag`iqC^rgyHuZ9uilA?ygq!!2R?NO)7G~~NB z%KqSmXP)`YeJoPxeOGRCv%;Q(e7*n5_j@~chhO?h#g%pUq-XHNx7|;GAzk;W6)F3&~msXzX>xZ4bn8sa2!7z z!Ll?@*V$<180D1ubmg|bA{}fh$hfAP!U^9w)Mh+HGh>#yKZ#QK)2BK4#i6rRP?%~y z#AoCK`4mah?Ro8|j$F^s7c&`nil_?#5Nz9p_GW810bYpIFN6RPdHKsI5_NX`4RsR4iK0diUD!*b#WR;rL93GP z(!cz=v3*I);*TrIK<|*-Z%0?@nsf*;J4>a@?DLfm!ZU}y#!o6pF?5i2EJHslaf;gjUkG4I>HQ2okgFH=6HOC=RM+wT|iqc1@Vs| zE$Bkka5vB;ZIUPt(g{zq3-gJ)evo8msmkg)jn;Zhs-KwI^ExdIBl% z5y-v?YDnuAtxs_U)`5B8>q^H%2uy`~ohi7NZVZ{$-p&7%U5Q^fPe>^5a;a&mZ5;?Nbf2d`kpjRl-lW+`&qaPuXW^K({ucHotM*>1ar zrfKdSOy&)VZp7b%GXb33a!%vo)$vW@y-N$MT^uPdMTK56FfP*8L~PHrCr)Jz+s34q(R)A)U(&1df{!kBMdU1l_PF3h0f$N&MxZIzYdNE^1?Oeyi0qaOtg@Ya8 zs(;0O-*UcgfH9lRRE-9DgPk(`4!^%ULql+o0X@M7i`_Exx_F?#;ftCZc{+v2*BwfH z#|P%5FDHIf>`p~m?uYBf%rYD^s7ZQGJS8Ay5w;C+{9I6$W#P=ER#L~d(L9dVkGXt7 z8X;T5gbC<~7ut1PxvDoJQ6n~sYsQ;Q5ne$(EdGRlz?_T&#jx&TAu!vrCJGrq@|&tp zwy(`F6I~0#4<4^Iy4PKTBYZ@QyU4Kr?OQ7VsdarzmC)4tvzSE#=7mw;zb{tu{&M4< z?oAyf3)rJt0_+}#3|t7hKJrp&n0FeV$kh$YIti@9PDIEG#x z0?4@3GXhNyCr5V?Q<{*%vFHY2$M(%+n>sJQgr0p?!cLOhU5ax=DBE$CI}F|Bhna}*O3 ziy&l_P53x!Nd>V_)5s1E<(;Vq3R$XY%xaMOjNbeh<-fbMU_I*4%QR5oQ0h*BfAisY zCYG&*{=ldx!y{7hsqYyyD!E!>7zXKC*gOUKfUF?W=XpvZ6je=r-~!4jkO(ICo$8zL zfNN{HAM6Qc?dOM|93LGnCSa5Fhm%QSfeTc=+hiV_65oRZYmIs`!cp6ikuUngur1~? z7K)CKGv**3m=p>})vD!c)t@0izGm_XaB6spj_T;_96lF987|~&%_Wd0u-O#;c=)aH zX~~!ym5pWfjBUhDlTRUv^xS9Ts{H6BflFq#U0~7mMRv;yEy8;j&FL`CVCF}Yv$!FT zxspNwvZfCeql^IZOc;$u$}r{S5P(V?sy&Fetj1UVR5{v}v!rWAZ2AU!wE_y*xUEn1 z`i6B-`}bvs4V%(MxX2mQCQImm@lc!B0WKcV3a>J$9@`;9`7)r#e9wtO{VG^zJRe5< zsh>4jchc@mm{OAwXa{wkPXyc43eAkQMGK{CNy~laQBAj{(`s+|gI-QqCT9%&(8wHcMMtyJcob z-^gHd_*(Y24>g35B=F#ase3!1PfHXbR4O`xv{)Q?DbdQajGWF1+Ggke+GtKMc3!R_9*1PS9UcEd3Fq$a!wu%m7NB1_G!^+VZZz!|swY>qXctiibs8xuT=9f9fRs$% zxM;n&LSiTl%Y)EcERO)(#b%F6=A2XKm3}p#Byq%q1z+U# z%9N-vQ#ilKdag^X zXy3cpooBgBo}Tj%44Ak|$^}21bFAV{fJ0h&dVjf+gN&_4vDoBI`rEr&uYirIAg|1O zpMw-JbAV+x&3+-g-i~*@=`l*_0_+>M8rmkg$xo!aXBdx$vo-iLDRp((v8Qgy%|F-! zWx}Exz)8~^9`sOM%@d6Fz*k_@6o%>W?)lnP6Dx9x%tc#|wBx~A%q{wiWoJ-1Z>tyh z49v&Elv>9>4ay|4asWpKN6-t;iD=(Zeq4V8xg=5lv&;J)a3BndTpMMMRJB5{KTlU+ z%uK_(!O#bcrqkiPh9K2Gd!Wi~HvJ%T8Ej;l{jfLD_4)JDdkn-%LaB!XL5*|EH;rtL;I*@fetj{c!@T- z(PMn$8_A8K%%i~eP6b71--f?a(9z6rXlkjrI8bn@0-4G93%T?OpKM~JKDf8xtEfK}_z{3XAWC2V>mVgv z;Qh-9>gBco%Ss1X&QA5C2J+M7wagp=vzN+i&Fn>3oe%ZA&#TA|Do57mxhAkjN_10W zBP7c#PT7? zyqfWnw-VXTV$rCTM^jy;&Fb4!(dH;FdHOyQDe31Nd@JA2C=6e2VsW-YaEz@!r{Nin zRIRz1=c_~nv>bGEn0N$jyv=NbRLagSWjwF8Vf@@5hUmymB` zGENMMU=KHx|F*W=mP7k6=F{}qWp6{KUa}C@Q*)!&CW3;9QXNfbif)O*S_VW%{4Tl+ zPALi8H0TP~9$iXw3Z*Kqv5vcN&zEd~P?yUYcO9%J0X*XQ7Ou+08;p))DLnp2+c#9bAoPhr_12A0f z1AuyNZ)yy-yq+tGhJ8C@Uf6q`SF`k;U}{ILGY+np31lCldI5koUq_+Bt=*XkTg`fK z$R54@h=Mlp$i+o)728!F$kBXO)b#e}Q!M^K^_QXZP{1LH*OdIwir{)ESVDO!W4zXP zKEendN6z|F8sGUycUkOWP~bd1SRZEbylCP@&edVqMX3WTdw~q7gbs;<_`q6dgZzpb z1^)9S5~`vp8$%e=?MeYpe*#ZNK!aK(6^3+ZRr6CXf4Ja+MO{evW8HHe{n`&~5Q>PM zU%!5}QCC-hEcXt~eS2htkc=7AS&Gm~@Ue)g-J zF#zK*?JVzG$|n=ofZD_D%Hu_%wBb@~16QbTjf}+1?kv>DW0xJA(}A?_5WP{Falebh zxGlf-n7%E5cqa=`no)e^3Ns`_2p+49F6*zZbe%)`F4od$&@i5E{s5_3tb$6hNqo6O zf39}PFo*lh#XE2sV+I2!*Bkngs2i=k^W%99S-Vull;(n3@I04KyAHdS>Gp72WjH}< zRwAq3`!({V^GKi;_mP%1D!2Zqz~zvH;ZewcbCq$UuT`BDXDVCynSQ2o)6{i+cnD!k zX$Bt+OyCT>$c22CJ6|UzwB$!K()t0y8E|AlIR>(s11}&;=A@x{M4LS&H8nC>l7Hv- zne@G5FA9Ej34q-bNNlemp3k{pbJ%EZ!u$b*AdGZqI3@GSbzRlfL@wU)py%+!+7#)hn1!=iST8BN-tkG5P zpr@4#YlE~xt&0n&6uAlZ#&^>#(ZEOe*B*T$Lz$qoVa;mOLe6Cw5f(KHxnML^)%F?? z@~SGh7d`Eye`PN^+8*qfs`bOa*|%I9$SRq8FkBf}Vh1qXveXoEYk;U?S&}`3u~1YE zsYp>&agEIGC=2zHMmZDAzBl^;JthlLJ22QOgZU7kIk!|6-=nqj%Cnuu!g4Vb!#D4e zV=?L9OLA3ROzl*c-=JEyIxp8l(mzrK3)dkY2Fmc6dc26+QT{6uHe7hf_8x|fSD^w< z;^E1{{9cvPlyOaqxC2HByo)x-!qki)Ru9)+p@)#@HHb*OaOft=-(jFB`s!?^2|>e5 zWs=cbeevwF-NVYp@<_2B^yAI~1I1xJAUW(x8(6WyFf-Q%TYPFB!+hrB`%E>z?&|Rx zdkh=&2yUyH^m3Z~e&2Iny{x!@2b|I7YHAU7O<&0ZF8p(qc4iT4eMTu|XzgR=2qtoG z{v)vp-5}3aw`|=w_voC#5Re{Q2HFkoPq1pDk`R3pt0~RZvFef>s4VuxsFJ9f2nzj1 zE&)Q?9JGXd3%#@QX87IK!uEb*HBc5NV{SrkZxA2B^R|$HGA_#!OPJ*2^HVnG@NZ3o zC`ta@JV~ph5Ji9l6oxCq)2~qxUKuPhjzy)xVyIEmF{!<6{f>>+*RE5FW7eutk1V(x zt-3lL>%<4(3*!stV&u;;OQ9ttm1WmtKP7GuYR9>I?A}kHgC!@20p1|OS6d3eE zO4#ejSZV+GmCghKy{|#d3MKCn(0IMvf1R_CgoBHtd#|dw_xkWs0&vwzP2${8i<6u# z7FaEitkv}muElq#ZqArCt(r@fjfg~^La=KeE#0e&r3>RP^LNxW_T6!GUm7kYp;wTU z=*Gf!psod+tgl7YXP(>J!T}n2uZ`PLafl>T%}U9|ni2iDvF=y=d8HId(#lJ1IuTev zqM(G#j%ps)Ssi*p8v8id`-Dr=LJt9l`A#;jI+b_iM@zaEQ$H~hH=}Bbgd8~Hl?#%-@Gi3bvu&8l;dG7F1hdK z0RwDQY_+It^a{u|uhPrd{7#iA^L6&xyacmrAApGdFg?UJjCJnBgK>Jvc5jcu`kvX^ zmZ)x>bxM!u2eLVH-+HZB2D$nXMUdNNP&jSs4Is|}Ld==-SMsY^;W73DBfOvwnQ<_( z1bD#`q@h)ZqGRP$gn_!cE^$Yg>Zaf%WY7(D#*h?AL~WGP0g-fUG;K$fOGYB-!Hz0u-vI4)d8s z-1kk;d5^$=kkT`Ezk#*rf!4>~{)czQ_clH?omPg4u%g3M>#it3X2vxo$ANl~bz)Ss zSIa={5kW@eeR2eMu5!5<#xm!!kO+s=LQn1a0?{cc_)3**%Z3m}19s^cq!I@xB8I<4 zy8mM}_6}J9Y?^Vz7E%BEs}Y8Cie_HDduF0{q}-0}DhyM02Wq*vS{+_NPxK&q2vSyW zcJM5%{TlfLQf;8)L$5-<+aZB1=s7L*=RK<=Q@nMq!8u%n3hrn*1lRKS_fyT`#E`vj z;C8SylZUxpk2c;Y#G;~->n8qGA`77{tiJRdqS=4Y)6vr*+9~kTE{=p*0zOc&a@qa) zj#h?Cw}pK8XeQBjV9=<&aeM%TH#~&eF}!w#YDBh(Ucb-c+@2Q`?qhy}v?iKWPtMFN zckuf&U`L^sm8`&J`Ai;^s~Ah5Rt%NSoc;lQ6W#GbfE3z zVtQ`R*tyydjD8V>_yJ*CKb0x%JsQB>Q*1X2P_yWyca7xFwOYrg;s5K zr_+Tj;0z;k$-8zcqnmeqBO@YUTK()t!XKdkMPwt-Dz&Tw3c1xwr(R}LoE{zS*Po%3 zHaB^X^1;KMhgzTL7 z1Xete6c>d=iHmzoRk@tyc^gu|pKd8A`aA~WApa@wfQ?@g ztam8;^NB14+Io#g`HFsA6l8Io!$wol=K$xTrhiNr!mONk#vD!Szat&o8K$wta4Mr> z$3t7Y(l} z{B(6M%k5a+Ym=tf0+Z48cZ;t&d2;Xh&bzk^OYB*gW5;_nKu=^o=&2}H|+pAZguGgVS6 zISl*SGOb;jVw7u@xcYAVgHZ`E#_|LDy_;?3iOoDrCTfw^)q_L8yW`X&JyD)ShGXiK?(8hnnSIARyLM%-ePVfSk#R=F0r7Wgt{P4(@t`%BjG5I79y} za~@~7ae_2(-2(J?fua?Gv+R1=opFJjp8zb*;YEK5Rksz>m^opQ8WXby0Xw`lCIIgf z13;nK=XHo6<$}lo-Mm%?=bMRcK~cP$8$?C9MbJb+;B`Ncc-fLhAlqOXh5(Y%{1VFKx7hSImYt3VGjA)Sx|evI$KpY# z<*iUW4Gpy$vx$}G@$x)CgEl{^)&(gjBhQ_&6L1?q_I2>k=OXAyY4mOFxu4>iLeo1NVjts|U?T7cQ&Dfp9x0HvFl*68V%#e_(ms3Ic`b&{tq z94$N`(>^4m!olGT|BvL|vO$|ZMn9j%(>;%SKgjn@GLIRtd>x~sodGx&3Xh#>CL1&D z*yp=Eg3Ybj$ei{UIhuoBKm#UJFcdbA_W-PJM(RV|B6u%?{=thcZBSobr1A}pmt3je z0nHLfYs&^%KAPo}DZbrbB7Z zkK`*r*3S(s83yT%q(i;b_HG3!B_>Aave-)K4-W?4`5e?LaM$-O7ZO1CpyAV>accIY z#xmU;*_)yC7%e;M?*ykZOV`Ym$s7xDnXgC`-MIN#v?`i5PL>}{6#X&_OeZO8<9H-j zNDrx&u(0MduOVoUVmdm2;^p*>+L#9cufZ&ouNSCfZY(LzOx*EbXCo*qUT@(U77w3P zI0oIUJ0Y#U6A7G+rhYd|E{k6rv25NH3??I*`TBsD9nGvuuvTaoUP{fwysHbk&drr5 z=NB>gAXj`>_3_K3X`a~P1-YsL+a7bc z8_*~j$!5AB19+C|B6)W0{&&eqCGnE6YE%hu*q@3} zBA`QtQs<_bvq`&wpM2f;({|@G=dTkIPNu3{%>}?^TXwPWaSP`YgQ?coKSTAq@f{0KHmC{;ngM)^~pO=}oq8 z^vu0bjDpfVHm4gYL&H=d2^VXz*HY(prVhw&;jx02gtX|BFUTclwlAauHh(THFTDk7 zJj1p)XQi@lupy+Y07Lf~EWd9b7>6eh zV6C75m5aH;_)>B6a3Q8@07UwWo0SmpJpebcp@v==c$fn<1N)>$^Bo8S-Tn5?(Ez8w zfj@cIhPfK#wv*>zi2DkNfde8{K-^28RV*aouwD%4QQq#fAx%9f^?3 zGlz=C=nvB!mjPW$c(B>Iv0<^!wsdiX?KIq{ZTU}*A(IbEt!#J3e?KoYR2rr`OomfQ zS;MwpNjExqaMIF2UiHIy0w(zsuc66rJy(1s;W*(!;#b~y@M0#X7O@ZMJxG)Jb|(l) zBOtU<4oW#~G{086tq0fZT_c2WEVsBN8ZiFo8Y{eH_jkyAY7KzJA*VDnvOqb&!mH<> zzbPdc{!k`O|)J?Rv4RX;2-h23v=te>gWQ&KPEClNA&+N93!4me_hX1Eo~09ncX z!L^&hx+uK0Z|E}W0CzGPo6$3M(pmx!^Paj?HpS8!Y}2c*eG=Y_8*=>(dv_`t{j4yI zmg@)42Da9XvTFps5cR+lsE1NEmRp=AaXmpQXb>ERdmaO34d=!d`T3digffv^EJ}@m zlpF>Vb^hEjFu7AuBHKqA(HrP$-fCgL^~>ZW?>0p!B6lYOmGAr*ES)?RIIW?pf6tit zkO|2^@BtA9r(#IU&RR;>fnMNlRVpOos~;a8*-%A9Ugo;V7@Ynmq*|fpuT-4$KS;&x z=op0FXYTABFYg6^6qb>7KE1Y3)-fEiFRDd>S+}a2O2R2MQ9CyAI z1I-uGs)p-R&5o_@O3zndKX)a0Qp?03&E)MB8MZz~)o$8o5;B*qxRl$bCqfjer3LcJ&z9H)O8SqT9Z2S*jU za{iX%YBkB!V1kijZ1*?O$`!jt7cD#^5S*}rdA>mxRHo+V2+>lJ$h#nRkQ5VZzv~T{ zkRI0i_MiO6kHpnCew?QWdfaVXdxe#*95(pWe4ax+E(Wq(d3kxML(pTE*iORh z$QVBM-LM_qv`)MmRMrdPnS(2{xu&2Yf@C`ep45NK09yhOVnkXi(WiU3^l)&DqQg+0IVUpDqZZc3cAti@f6>z=L6&G!rv zt~*7#tuYlcxtBR1SqvEOa?;U6^b)#a?$=eNOt5~wO!`(qxu8DW2f;gF^gB3spEw5! z^(B!RL;6?E2#w)>uyrt_D1ZB`yzCF7O08BvQ&wg#d0q5x9(LSbsM{!02dOLQ068I8> zu#=FKR_T^*Q9)@Cq+1E45eeycEVk}_p6Bf2|Gd}rT<@3n@xwlQd$_spd;Qj$YtAvp z81ujaIVGmk_}yd5Bc(FwsVDBN5?!ABMfIMlKgb0>g3C+`n@lp;!c6jqMEQ0<7Tn*Q zQItN>Y8xsScuD7p$-n3igu#c@!%7^AKB-kaCSHUkaMD=S>vl%SdaFk+iXj(yx5!W6h?0%;kbE`gq971O|d>lWO(&)PTBxns$ zrx!0<^I>se{o7EVi~4h<5%%u*KwGW@?gmuAAza%5Y1x4d%f5CD=U6d{Y~99QyuFmT zxauprj3MuF8%ci+RQjdSW<}}Co}QJG@;3FB{jjPf)13N<_G7~>d1*V=(2+`{ev zs<|LSS;IUizp-lx4-DJXp#J5XSl`M-t&%{SUgnRQK>2;Oe87=6XofhX={@zjk4vx2 zzQ04rn)-f`q$RZ3p3z=$V$|vuFVfmFwD@SPJ2!qDhDUd2r6QFq^r_FDd&z zn6h--Rj>jm*32G{GNmYex9RQbCyL%09}E{epl8gPQ=RMJkXE+xlk+|HaB*J_QHv|R zB_aYqt#K8n%jamCR2m+dus?O=&l4f)QoHK9Y22Fv9q#QdpR?Xugn?mb_}-NJI^{KU zD$kB;CzDQ#*_a7gsnl`_AAxIUdHMkql+v@B<~L`UF6Mt7c%Uu*nr5!yHwLCX>rKC^ zU)j^)XORE^Dno0L-DH<{1!PRKFJ5WfWSYs~%_A?}QP)ZMumfH2bW<&z2<{tgW1pl-`= zM9Ac_{&xCxMgw7-XCg|Al72@=Ya&f9kncq=3Sx>L}>)m#3fMC{fI0k(el*X@E1ZerJ zUg8F$Iv$M#a$OkTbL6VqSyjQfvceovHFQ>UWcLa`HhQ#3Q$kO#7wdgsmIbv5cy0!V z9cAzBERx5Ap#}K>AWciuUvEV5nbumBLdXiN4Nq6sbLbYLvp{+$NMi%|tW6+wsNm^4 z;AFLpRn-b9nZc3#W{|u0rgfn9!z|N#;o?4fEWyo?2F}Abko<8}68H2lBA&q!ij9*< zqC&i#X!$dNE;PPF0{vOx52L7WPMuNUPlyKz*ptZl_midg_7GakU3|U;YcZTF3JAD) z(=N1kzsEaJzis%Gtf`F3j- zz8%m|Z&o!Ppx)?20Srlds0c&?Xh|Hp@U+|}`TCAD1g+253kGcl)CQo3Gp9v@1A(_2 zs*7)f8mhUGBC(suW4;k}1w>2F%!JaGIm#-yio`{!Wu<0wHl`Z~H*Nas&4rqG{i%%> zI^-j4B9HzWs8O#7HV2vh&p(dMzr@l$D-OG{>{pCy#-{^u8T{Uizsv!koEHND`UUO; zdPc@#skDVx8%3?;cZ){O#E#>n5>7BqB5z2IG|brpNYy6THSMmUFJ#GoNDlxDsM1_RBH z=F1lUbG^3lPdft>C#YUP=P3@)#uu31nYuOxGb(X?pHI?u;`%}WkPPbokatp_{?oCb zaOXNY0o-_EEc(>qxIPOWQxb;f(2d_}h7Gn!WsPP<+mqhyNX4xx;Rit-PDi9&<_v0nZ8{Uyyo zuFUq#Hpl3M8L$(?B_z!KCfon3?|m#(B7mo6Roh(b-%HlC>(KwOi%u0RBhOm4ZaJMD zkmb`Xcc#@*j_y37Esj2i3h$Bs&TSz)0ws?>%1_3-Dqtx(@8o4wPTqI$U|B2`)&KAb z%)k!M&p-7`kuP}}K@Ldj2YLe}=SWxK=gOES^Yr%%xNrc5LF<~vIKM`0z^kp-N9Fo_2LB3WJGh@eIO%qM~L4{C@d%bzbM|6Bs~ z_TT@t@Nf5N5S0GcKf6qDyZoTiAEK`cD}0!sguNO$L5!%7$+Evsc$`_W`TV zA3ze_U6%{d47viAB^x4<02IkX=}DB4Eq@)J&u|l1_!X{V{?{h!$SWmeWMt59>zZ-_ zW&YsFZH2e&QCF4Rv4?!~?^E~J3nkZrP!tGhA%J3{Fa!|~wV!V7caU0M^+rXbK6ygY zL4C=~fJ{Aa#h-hAQ{|#A`0arjM!@81rn_%x=e2#y2M_^{ruIkyVvzE*Qssa^ z+z%b4R5Yl-^UBkIu)z+^44buR$F{I*HXuW$auv7WCZ$B*ntk_+>`MnDrn-2XK5a=!)B#8^yJ40N; z+IWOaium3qVC)sHa<|_ISWwSa?X5b$-i_~P@Hxf+lYyH{S1SNvgcZQJa4;x32Gj0R zAV)zX`tcy|R=igZVDc7r7ae>%(s)!zM<2WS@)9HhtZD?CFf=c5l_*_21I0=D-(QIC zuP?;mq5_xz{SbU(bM0v&K20-FlX%yFAt$!c=2jppj)KWnQUOG-WlyeMH|Ey$J~mea zLktXyJT!LFH44_DorRlE)xsPi(_&+k)XEdrh%HB$cDi*l;Km+F(v^omeiFH23I_wj zyK6H6+UzOQnuXd|hW zVP+h)?2%?ZgW;JzSXI6VMYR>_Ux^3TFMn_KcGhNO;G}<}-|e)Q4Fe}ZtH;g}K;z#<0wdrD7ii!9O+Ci%O*$-4!0L% zMaf%{ym&H=qwQ^=AD?48fgAB95`PQ#B}2x=o7v5K<%zz>-8&oi z3e)gfTtm&H&3PH4>9?a1GEIy_Vo~n$x*T~B09OBprEGHr9?JEogHV4o{P&pnV z#Qn=1r(8dnAc^ zkx)GloyZ||bcPLbXUB=C-MtQ1hlDR-zhleBe}r~DQAQ3GyiV4^v)0YHB)0532lMYI z_tw+Zz5u@rr|Y~Jx*%={h*{6O*+J0j$ACU!giyD62K*HefMiqj1|?)jk}$ZhR(+YUZA5>M<8`8aa#~ zu5m9o2*aKktY-&;L$b&^p+E;5sjQ+7y>gXx8rXngW=POcTX_#u8HYSl8kiHah|@n!a4&Pcf%(@k=Py#5`ZF}Z}sPkYbG5@sYm5tSHPr%iGiVL5rOm)g8(C6sRS?P zY8rw-5Q;|c_Oh_`R2*ONcka~tb?Dc2bt4s$6TZ94x*7F>ie;Z|*00?B{?Wg71L4}q z8WRD@tI4`u!p=02Yy0Zr9RN9ok7j%cJYs7xl1`KZagS}$&%kU;UW+F-U!UJgz66?$ z{|g$hwDH@U4yJnf5v5=M0HRntbfNgP15SFtMG%*26d?1AlBJrly7|S}WhXC20IYi+sw6{FvyAkmeV*4dAG&Q zBtkGK^lRVzVFodVQ$-c>#kMuJD>A(O>6R_h)v zM(pXVcNaFcS%QmHr)C*2L)(?ArRwIplEXzE zkgpA7=HQ#Ws?$Um%G5SOYSAwsaIOB3KBx7FX0*++SGGsccxTaXMThz?qbbK@Ct}=~KB(!21j(et@yF&GfD)42 zzY*L@{^gLjIY;U~j4^m{M{{f-o+|kEA$pfY=J8X$50$oV-EiFN^r2@Qryp)P z&UGbXB#Gk0l065Og*n1w$Awdyqs-1f+?BH@pY)(PYWm$AImx}_yP*t1|3m}HrNAK2d_Z%7YxyaBy(2F@s<8-ECA&3;c9Nw+|f zqXQ?Z?!xMX)WU!xI?k0^UB*agu9`i7AnNQJa*anD<1tn}HA~YF6B2T){%YNPM5kQD ztfsE+XsEuyRtE7vtD?K4>mu`JUXrzao;< zCpA|X9I+qmW4Tgb$itFHXY+M;()wY6a@)GaLc;k`E36i4^H8L(5o2xNr@glM4qx2n z!wZNM#aqhU4IQ69$2dlY!Vtz{u4AMq@#cfxE81=P1bn9Acd;Gm6xD^IG)m!WqD=Vi z-MekFsK2KZ3BdHV@*qLBr6hxB(D9D)zLAiSV1vcWxJo;Lt47kovPLe^SrB5);F4mD zO)@Y3x(QS{*PDbQ?^L_AIzHnP?jxDtFUEwGp$ng-9EGbgK@3`-K7CTqK;{ubOcOvQ zO3Q^!!2SF8c{SXQKyysVjs;AD1x3Pq?)75d95l z{r7~Wb^{?^m_op?c#JR3+!K&)c#ZGFZ3m-p zWlc>CB^+Z|sbh_g3`AYmtF;V`46XDh-Zg5OW}Cb0-D65gNl~DOD?x!)u|+9Z zjKp)9XNG=_P?+nksT)bWi!57-d^iNH-A=HvsIK^M4pD_!R`ESm^q?Vllp!rLe?xB# zvCo(!{r731Q+cDo($BL%!p3-wZaN-8UAb_{LFHsuU+~t%#I9Y7N&afx5K?_ORJ@`i z>Wl~#T$6%>p6`Z7P*fN=T8-Z}5%&$^Ghy@uCkKmlGa$@`i8N79YyUz{zKncKH$ z*GUceCE1IP?4(e=%dp*OOVYBz6SQ>?p4Km9wohK=Za{IMhB;mQg5Ky$p~vNC9=Unb zraj9Mfgd+mHsS6L6^7dO-n};NTXKiS+sya5T-7%OgXvp zINj5#MR#x%VJz4X-a?!ygwLGuYvDpQ6AMVdqu~jMiEk>O0n(IEV?1zW{XIxmp5Uxp zwfB}|H<%zn8y0eCu;VdMdiH+4)|f2HrU07|#vC;8k7ORe4U>O=BMzDFt0CQzj;O2y z2ca3e^11O8tUg$@Y-knLmg|Z;Vb@FXwzgGIuBQ{ohLZvYm{qeRX%Nb{H+dbEx9I~l&347+}pxpIP!i?qzauj89}%+gCQ3 zM;3L2O}+|0=n`ExmomUK?$WGSzeIp8TQ1~+#}_= zE-J&aS4{vB_H)|M9%W<{w)i+YHkKpnt4GP;5>3^0m8g9~#q{rN2l0e@fU9#%G+U7@R;F`KY&<5`8;zgP`7#ry8K=-MMw^RxJiu z3WM$r^k=0^)^FM|9DOH_jKZc@g9Kr zm6VB7Eu1W)c1SamnizNO5?X}~s7`P_-PWJX!)bK&qWP z^}Tq$;w=-)^dM-hZYwkJgN>b-fOJL2eiZobNWgAs0jYq4NJw$E zh>C}-OL}@bs-fw6E?<7jL4SqoQb2#`gSv)>49(!pDVm8g-oWfZR=4p`*Q;=DmnKx( zy2P3@rI9x#zTf?r3oo=7y4RXgV->E)gK~D@o*Lw%4LY)LL9Ov4JKt;F>*(g`ELsZo z%?hVqQy==c=(W~)lSO9^Uu|dYG!D4`V8IG&!^GU`%8H7L;!h87tCJK`V-=|MdbE4A zpbvM|81JjA=ob?Kr9ua`h#{{FJCvup1yF7?Gw{mpc~b_{pSE=4JY4;-gCzIi`w+jE z4+%oG%FeBm-JSm#1E;mB`I43P+C^?!I1o18@$vC`C=jL%TQWn_R*$_@^79K*q*atY zs@zyp)HiS$w`f%#zMBDujEO&FO60#{r4**YMqX}|qVRQufOu$za|C7O_w5-kN% z4=SPvZNqP889D};$)s2n-yrZcoJz(VfmAhH2#lob-jj{Kj-X#VuIL*?$)O;{G9@Mr z0R)Ssk}B-j4;;*E9E@K()X3-7J4 z^nUE<=t!xUq`P{i6=CwJePm$=V{@K;t~cKxy8Fln_o?;zMnn>t6PDrZjNR+n!Q^f)NL^|WJ6IXetzpB!`I@=DZDQgEv0!Eb0f>Ozm+F>`sQ6D<@_B>cN<*LL{i}_DuT>4qW^2;$Bw78 zFsE^GnFqXW+F7_ehkT5Az}gBUHI&w>TA+{0C;|FGGO08`jvX3wW<7Ep_{_06*KlJ+ zgA^BrB1q<9p%ejwaqoP6j;*EX`4cCQ&}H_3av;Sw>VP3#lkpC-V*K|MW&s1m zs+qfI;?l;*T@8C)ofiC57d@|eujY6ttBYUv^mR5ovnG@0I?V@hSROe$D>xJj;_w#i zi0DU?1PvF3)YfMT&w=zK+bxb#bn&9C;e>$TU}(|hmalMas6sMWlS7ihBbWXu8N7V? z@`)2Cs)MvK6oG=`bqN+S|MhF31_3V5Kg09ZIQTq*!}ILfGo&#DsX|7B5hD-v1kHOr zG>71~amBf^A7>qIhI&7Mw(Hl0nG?L zg+cYQP!Y9Dky2cmMDW+s0^M_ij)p@zPYoEx%4*x>Kjb>hqrig2w_K*M3+=(hPAb>t z9icyVZ-o&BL{^a<*=UH!*nR!5JEsp14Hv_Tb?Y$5)zW74>+^M6X=z!fJt4J_y&IAB zY`wIck6F>IgdQqGnL~h7lPVZ_crY8=r}w@2V6!zIGmfIidJ@3-_9I0-QqUYmn*_>u zM1cm-h+f;sTZ9i5U~!ho6dg2_;0Q}5XqNYS5G9&9Kg@v9>*h@2IBc$DckOT+IQ7(O zy${^jBAhkMD65RxD*UR8Nc2F5HGY^~xS{RQctoK&TG{9|*jJKk> z9$iQa0Wm3VPn8N1z+sVd@PPs{GhDkx9}NSqtOooO{x=c1;{L}fC38mLeivyP0U-IT z^_ABjarPJw95~PbP@&{MKL0;E=gses)6)M@521I``VT~PNb8AZ_pxUs7DKQxFJ!uN z(Bw!2u)JtPkG*Hs9bI+_x-TY6rMA)?!*+T%M!tVvy>1=X+&jWtAb&)*|Ag^gtBv;7 zh<&znXOiNYrjWoU?vjRv_+G=8by#aTCU-<1Fav$;DdDg(4g0MZuPWhBr$3%ijMTI` zJlfO>300ZE>kb6aSOhbMvmV0}6YJNmb$a-a{TB^jSM0CB3AX4sU4Xl4gBxz}d5)|b zO=v|hi9SLs27yUM`{`ZZ0^sR6o+|ZH3K>HnugEz)MB;`Q77+8{T$O%t)FOp{ zQW5Cwz-<-~5CHw{YGZQGv*WkWTo^+~4dHzsYQjy6FsHZ*k)!O=umCJ*6e>=F`-ivN zzGX`YttTP;;RPf9`*7s6!;3f`J$mF`avtFf+)z@jfTTYN`7@dYq5KeD;jQI)8r0yv0u-*s=Q8 z+>5zXxHsZiX5%u%tRBI>M))yu?Au2iG_;ws>+!8{S&adVBW{KGnrJUmp^=P?t5omU zAjnq4$Qw6s^%9Sg=rGNT7u(S~LQpeMSe(8bQR-TAdVSw_p6n zA8+5j{oK(}7W{Kt4cq+OR`~A=`agz0Y0q$JUB7<)&6_vG5x`}ED1t?P^t0XldMkO} zj1&~Za=>{HBIWbRNhxV-6Xf9F*uTHnuJ0$kZ&(1?_C7{NMtb^Fv$_@w@0R^|O8-1= zgCpAg1G==zvnJn?C31Nad=s&8HEw0m+EWjQmXXT)~ZbKw=~1v`TI5~WXmV* zdVEdVNJIp!VvoalsTQ19K3s%f3xKQItiXBB9TZEV_q4Bl+C$OKejvj!_$jB+_K|eS z{QQMzu;Myo!tF(*>5S|3s9DsZ8r zqomZOg>5qh{rle}Z8z1SMY0O3zHN+3v;h}C4lGJjzYfC=we=J{zbjK_;3W5YaSP{T z6--OVokxTX@3ETDEK%fbBdQqodh< zi-ZdP$FJwCnU{_p2_2lGH8n$&0(kzrm#x_Q7@mscdBmt@unP#Z9xz{m^ySO=H&stHEqve|M?LX}@Gh|-qc18%6ts^gR^!`MiqYw3eSi!#_uZ73` ze}C+64Di2RkjrP$D5Ta*0@WFv1Ow81Xn+OOnxu-Id;@kAQh9($%zjt*|G4wijkGul zh=@^}uDyj-HGNl72a$guWeT(qa18(SpJLI!#eSyw6OWyo_|Zs5)Gu3C5P6FxiQD+| z?FR3gaP58n?p@JJe}3EAep6rEtwr~GVJo6ryIWS)Qnh6Q4Zpnp4*(3_+nsX*IIuNx zB6%iy_Pcbr{`Jj%W=blKYu>m6JZ-o6hp)46g|(kXD!>8I9?UdQ)?b3E26V*JF~ADo zwYqheCmiA<*iAL}gXE)D{uz_?)4+uq zALJJ7L<8#jND-4D2G;;-_sjcr;tCJ^JooLV`!&1GV6FNTU3Bgr234*F66%3o1u0vY zWi*k0b&F{3DKG4>@dY$-5U?^0zfCgtFG9Mb_>G_Rp9}PyKHMc}M(8@Hm6Uu>M+M;@ zM|sph6_pFYS#t0}T9b5KxOe#6IceSO=twaueHf1a%``ofGCl=lT*I6>qf{WFs*}1n zo*m_p>7idAmy+&hkQ&X>|PS*RPOw174T^!_*6DXRp?H=x!HV_!k-Z!8^mp-WOeQQJ+MV{n%ZU zOTF3D71U5Vx^Iq9N(z%_t6oldlEwsW8nAtmbw9rEPgkK;gpr=Fxf0UmQUj;V)u^{U zH!G;)MSj|+FQaiF>j_$3qVk@-dut9VIQ1R_jsAn4tQl%Clo1iT(R)jw+Ph~@Cx>K^ zKx~cgix)4Dvc(lw-;3&a+VX%CKHd=A6OvxTCvl(=O(;Zo@{mAAm9RL0p5ta1E-7Ga zy(g+4Ln~w^QOZU-aIjqK>?I&f%V)H?&*q%YjdJcbu35Zm`6OHju8uR%qZfAGCbs9< zV7l+iDl3qw^1F&;E%gBsvY)b7O5#nhE$Z0c`^4)HEpQIB(-^t9EWkK3Q9!OOozxT~ zQaV*z>ml9e=es>h3q|L9W$#U6hobM%H;>CjY<@}ARS3qlxN6}2s5WdFxv=Yzli;Oj z-A{CwpF>XSyY=U`a9s0i%+`+}ESSm-?v1px2HQdpthH`NM3K7-!`q7fq|j@(Zr-G| z8-ByfWJZWfZPs6QMiUhZR=D?8^5XDX2*c2)`T=%f;c=La>M4Zmalwe)jeQ*Gl3ulF z`XgY`qD4rkQj1R^Mya4!zP3#^qA#YH_WF-YW2B7(zo0X8n2+1v)r$8;xwGM8OSebx)V3`O9C0KR(iP;zLO`}@ck$d9iijV+WZ)tB7w zMt!V9wOuVQzx@qQ4r=b?YYiiy9>BQY+Y4g?-qUEC!jPQ+>w8kYKtLJoZ#bfB#D|#? zHa^E&jz?Ej^2N>0MBB86OyF2PaCRR2816EswbeWTY3QYOVj}l}{-BY9aYeEqT;*Jl zR-c5B&*}dCLk0Eqfn-AYC3fc-=j?i*Em z{#{+2Vp%ZGicbw!uXwe)E}z=NsagUA5-)bsUq^q}t)G$?in(xbXoXMpqu;JAU0ik4 z#TGaFT$(i)!Pn8oTgO_ z@H4-?5bY&J_Nk=C$h$%?l_B%cP`SmgFM0ETi$695+?0=j?d9V#;L;JrTc(w*Ulk=6eFXVrZ2_inPr2vt&GJ(eLR3pwKU@t7824rZLiraWB>z^|Gb z&1Z2?Fyn>ZL5j;Inl&uozCMU6k8da2?5|$Tc(U4?oi-a%FlA54;tokv{s5qQ*--nR zqkA6LdvZNLy&ASD8Un$GhY`f8{M^tpM9ukbho@26cTov+0_`G|FcPjor|GzojqC`f z5cp_Z>uoLX0A_xQ@g10xRUi=pr$*;VE)yk*drSW$_C^BZqvr^d;68ceGA&$^(1-GV zyMHPQU^s^Z@sKySE|V!E8(_^Z){^FyUvNj;e9jmMM$}C*Amn|BNn(MRANXFJSxuzu z7a06FJ3D)g7Z!?)xqt}=qQC0rjVnnLWjB-$hcjhPs@(>D{|)Ti3JkcSIRk70q}^$# zd5$ixM+=J=?kLQKh_;E9^@&`))OYUP3*t6qeB)(JI!2juwXe}q^vg5&VvKvn6G0J2 zi1wJ#>)uSGPbYAeG=G2rn0(wpCE@a&+r|V!f90EL9QBPrXK;Fe^^3~@OUe1YF45(= zDEz9%(B+T_f49$f}@fnUg;s#S)_4u%q%d%peeL~mgaEBURB zE`f$CH~-zc$Z=e?2rnx9owaFVz6Al8Rc02+@(}$}v1v&u!SRmald4S_0GuFb~^loFFibdbVMPHh7d1`a9c1QQ_8>0w0x?*#5Pfp7Vn zO-cc&N8CF{M|J@S6_&WKWS$Ybr~XuUA)y%rpcI{UkvRzp^KVBDTbluSvGB&x9x;9D zRUq$uqNN#Yc3w?SX8DpOB12c>bHdh6`PqzUZJMk%>8!PQOKzmHx3Om!diLt?R0uFN zG8%^Y2&#L`L5k5H1m?u1rk5CT?+o1-*oUHuXhYVV43(5>#H|E@UxYvxF$}w~{^xlv z6#A8TIBf$Eq5rAp%NHH`bDkJ*#@8@FKOllB2RIgX*Ho^DjRx7*eZ03v-@5t7k00pi zWZ-%Q>A>UD3&d3*OPE!6i+k5$lI)ztvvpUGRsR$Ry(;}%H8rW5(gZ|D(qRJSHV(rb`m(FS!oteO z>ot#_-`e{gcGo<+cD=6Omke^={MvPlgk*pM zsrQp?=to%(UwlnLS&tTDaEX(3GXe-o(5ik(k(Ynu6Yg+RaBwjCwee}^FKcMT?yy9y zQ+ajXuK$a1&G*UAkJ!%Mu$ie2Il{7O6JY2wE0;5I0aBCCjXS4#9& zc`ElD$2|=!KQR@G!VP~tAtYo6ie84$;(P+wVp_}IDvzctI#4|tO&6=Kq~%re4FUzU z1(Wv0sg;xr54SLnKo5QI&Ye*99Xegci2GwkWrUK5y$|Jpy3Oh>`-br^ez%6Yk=2^m z^tw@H`7>UzZU*7=&DTvpJUwrs2Y%j3!Q)WqtP3VM8S0;j`eKY_>gq$_|C0Y;bK`wp z=u;0aCLmW5+scL~X+SK5H)VpuhTge^?#av3)oSjumgx3*ariRBt6?vlW~XtMc^~5d zo>)!O!QOV9yl)9hEu18rq|w)9G;1V@5}1ed#L?G-^c#uLHAl zVx9h?(Pd2VLBkN*as+DP>Q;quVzs!YeQ(Jgw~wH?gmg#P8yp5SDk>RZoWE!36i=|% z>V}P~{D!pxDVeaJ0Q{!9j0#7+Q)C+G@|eY=Oc%Gi@!(C>q=Q*W8LF@>cH7Ux;`$4Y zdrjR?)In@xh#c>v6$e5r60~yzNXS*w3<4@W-Q>EBu~<<1J6v@+|Gb=7Q(%9I=|(qt zoVQlxC2dkHuFqcCaIBHZ2J=Q??@nWMex8__s(u3rtGFJd`XasL@#d5yvFMc3ae)P- z>&=m>S93O+Q*@;2I(y9qYU(VEO#`TmKD?B`2|4t(_U#i{zqzdjAwjbrjNL2aEnF!6 zA`g$G1M`$~Lb{M^^yy}6ToPBCTDDa}>)<)sx^=T*^%c3eY;kS)*>ly};Vg!N8P?Aj zJ$R?mVBRix zk2J00GeB<&IeXcq$XYxSZs(~fvfii)^pjO?-nymL4`PEMCn{$ZDo}P}Y8b1+rRX9n zzo*mLk3QiD+d{FGxJ@P!=5>D-xu}Rz1X9G30Nym3d?s_10cBvn)Rbe$;QZ{29}L-Y!AYz2m6>%?|?xmx&HI}lVHP~y(OZ8bGl!z{^j#E9E^AVB;dh_=j_Si zwik?%G&)cck?qTGKb2g`0QZF5OM59(sfag%6Y859z0E$HSG_>7Q04RsNkA}Gj@1SK zRc0Lb-gzfdWN}0-Rp9@!nNb|1hH$PGlzz)!LFDW7PjKgpkKt0@`=s}T&ND6S_I7^0 zneCrZnWSE8*tB7TE9UeSJ$!(E5L1hLjWa3UL9{_*CDlDSLe>U!N5cBRr=lENsaxDZ zyn(^7Pe`KcRa3HVMj1|%HGq<)m%1iiT|JXTFkA~y_i{$T66hQ5!wB5e{v6SWaIWaNw$oiuuk#n!`2K zp-&H8dJAR(^YU-Thg^7VMz2tE!j&Rvh>V^TBRo0aupx)h!R}V9ig<89i_E*S5Gfc* zCkg@Dt$Aqo#@b9zDzdml7x0^p--|wDhK?N80O@Gr)CF__JTC_vX6vW>fKbmJ>JW?& zj`B_b5N}j822+tF>5LwJcoTA5l%`<9DF?_SD~@$fhzM`o0Uj#M0X5B5a$1H6N$INa zN}(yB94&E5O@-{W-bs)5=zmkW+$;Be2QVUlQ-R;M*H5Y;6>;&$_rVH(8-PRQedGpl#YLZqLJ!d zp%8m9P%zb7|I~oEe0kyGJtaJO$Ev%OaxWbbarm5OHWj9=15?Cvcz76?JaPBt;qMUS z2rA}_1Hp9%yrj_{hz(}O6)-w65p-BX*vu@Md)LzO@9Db- z&Z@fo7##JG&84JnhlykOmJkqwT4Z(Q6osNC@~32AGF!Y&%9o>vho2YqYUECKYlKdJ z9`oMZxiPlAhH40L;LSF)$sJSby<&13H=wV5+D^gis$`=h{)Z$W)9gMf>da$0;7ZBt zGL1|?%oaYO)%@TI@tdG^oBB!_33SM=o!`^X!aN~Fi2n;=3L%B)qAYY{D1l8SJD@v4 z-vO3Tt0aM;nRmk#X<@_C=J_7?tX|54^Xg|SIskU4QK6#}`!VTc+xPlsfdDGD5my_u zOHIU6))7}0?VarRACF+`)?NH37Qin`D7G$f!(qSV1!l%u?A^mU-Nxo8@2+_w4Unm- zX(t$sAR?ijZAZRdc>W1AgE-cx)Jr72B=8=_?;n9TsJ#*vBD#DiB#Q5q z2lzAK-il}4`4SoSp{yszH9gRKN?-Q7p3eHoJL!1%plgS2vB);*ItxlR{RZuX&3fX+ zTGq|rJ>Z<5blMNM`qt}%&D`9|c)Vlda#O&QrV(||3e~!yB*g3jiGWr#c*+)WfqJ%| z47#kgL*{Oiwbh6)+(#{1NVw(1s-{@DN?7<+Em1LG$3;zobd5VY>k5x)*(=ubXO9 zUlCR!hIVj-jT-KRDJ7wMEqpx6s4-Q}sDdBJVjJ(d&_s*Sn@89>T0dyts-edMuKlz!N897^sn&1a~(Tw}7 z{lw2YwO72egFuPfz-o#Hb|+K(_+rqt*FHc1u)+HV&RSU!;8e>)YO2fF6UswT5-N$$ zdNe;fyNtzd&x6{ea)b1+0vFF&MVuJmx6u>(fsKMo#s(SayO8Hapsp3YM)aGTZktzV zn?PZq-r+we66Yu7#urxw*WRA}N{O9txO)*CJl0C2e6YQS zE?FWoSPujPt#&Z>oLC<*bsDo>$bNc6N0P27RuC-|g}F3Pha=8N3s2(FM6aRd5-)~} z9(PU-*eq4>GQN_d#tpfN=YIK+IwrO6>3R=EkMY62t!#YDm_*$eZs^&UYk*K5)I%9?9!@?>+`$#ot(c_!Zi+pm>%M+NAGT9>|m|vRM?gs{MI8O^5hBNRMey-ynR-TXktA%qGj*F4tO@Y9UhJD z(#25MIu%zxa&l5b?*+Q*Z;(Ow*+-Ulkg26^GbKJpzCfTIGma}q^bdA?K{D+LgFe8! zfX&Ny!by4$1rO1*o6MM%lEgI)RaB-2P}&HwkhpN3xfnA7;~!i#*JX#3IL{7uqy``*k6 zqHsc7dbE{ATPxSC7(gK+BUrGP*Gb;N;tQWX4FTj;G`K|gzB$Y)_LXah1e>pA%`aQN9M-oJ znGU+DP+2G3era>&H$zM(FMy2-^s;*D zz*5pM&;}w&)F%#@P?*JCn%YRb=Ul@3PyvN#PazwRnIgtDxWg4>n$N*j3-XQL?Mo<~ z|L34-G|rX}gzn|bmq0_F?qoiOS)vH=u<_btrXnIz2wooglG?M|n?AOKc2&D*=WbtI&lD6dpLsL`D$X z<>Q7cdzBB}$9_+6xwz)~#<7RwEYGzpt>J~|D8|4kwCwF!wSK*~G*LX?qp$y*8f8z) z+pKqUT@o+#6{g@WQrtC*hD@xJEqVbOMJu9O^Y;#(v($mBTVhHSl>n&3NW8wP%-h@~ zdQSgi(gCfwU23)})G2Y2xqc%9017qNeIU+}MCF;8t@i`-4o3je0MU4tZcIip0rKli zst|B5iThflX1+wXImb4QYu{t|G-qP1BKw%%)tVYKpx*>gKjx&8%OFymki$Ky3R+d6 z&es-dzKb9ggKvrZU(UPv>R3-NO-M^)r3GsC2WRL_p!YDZ?0mHzfxKmuV4B*M`g)U% zNLe!P>V=gk!O2NJ<*9k0qyKE&a>vJaes>VRU*Ap3W47wicTBu9xx)3IJg)U9!Xjcg1Z`^@b36OL^L9NBioe*zbd$b9kEl zK1wMci8Mfcg2)hho4U#arjb`_AI*Loa0a%Xlk6K&FKCYcYM8jfqjv%5V0T3R1l}DQ zfD)I}U!aSF{Fp=RhTIjnMd83c9x%k5c>&ZL*g7~?niYZexcO}imjUVbKx=yllz-uA z!WYKHuu=&d!7r;Q$BbT!&n43Xz>-GkCi#07n2k5OYzq5(t8ev{Q^idXflX4^tMPuKuL)XbUv(--oil zOP)J-PDtoA;jh~Bj!gjH^vi3vC&SDG@1cQ3NDvU*^-4pFVGp;Nm@_2P=-#og5cr4N z$)Ai*m(6r^blbLV+pu96z^gB}svqFtGp9~H&2yBCMy};~jAIMf)%dR6j%&0UlXbh( zGVyS(!Rb|@YWCcxfIk*5E<4ki3ZCOSN_Kiy)^eI(c(#zqukU>vB7!Y6H2vr}FdA`U zlf1I}_JVtp(J58cW+Wu!D*?Yq13(JaA(9$>a2nCV*g;KwRY6AxVZjd(3f2VRf)yZh zpG1R`2snW)VusPmq%E!bh_R&Rw)s&LECU}^=%_$(D}Q?Umewywx1G2!l0WI{a&mH4 zu0-VK=KADFkkPUT37iGLJ~ak*{j#@7m$0+5JAeK>gu~GHP-aV@`nCVz68`s(2HyQi z!!R&HPq@APqM)GQ5fi^e!WcjEYXicne9^C!;E|S=e*XOV+}xaYeQg|eH}b{_3H-zp zBu!ecMt;7=it^WoEkw1nvvC9VlI9AeBuCq(7QO_ypKE?@YgHz6`SE-RCFK^Mg(q@F zTvY>$C>+mRcKGE@_78MT@hbKEpt(WN1$i82QW`Vi%AidevJ)&5E7uTG8EOB+S>>sO zY*5T&-}5;R-i6O{fiYD5{*xz9z(5Del}VW^vF-`pmrvm=`4y7?^ecX8a{Iv=a&b|| zL`sk?FfcK)%h3HeJ-%#(<~(wc*1Nw6eW&(2?8dl+1N-(xAURWBQg@2HaJ@${s%EQ| zzvEr5n5Spic_xSy1S083)H$kIl+)D|D|3j9N^%&E6iftr45;i5hCH@zP}sEHT0{CRa;{6(@^czxmh z!j4Rd|Al&+?HO1QtClrhAat2%lgDmH9DtWEfLKiba5MYrqk4{r2)(c)$2=hrj-d9wuGXpI*k7oca!=FHZ)7-xP&=g$T$$YO5bfi@?F}yG8<%PGy1jpIKuTPulY9LG0rG`M~y!q`!3GGZ5AkH0-7C)n&#QlDQ18 zQjdZZSUZ0Pct0f+vg~ITfEOFN{L5Me-Ge0#irdOC$e*>D-ChPAHFpCG_A8w-^7qU% zQh=@W0YpaNB)a?6D^{&?%V-7zcJ{K{-&UoOr)cZ;VxBRofuGFo*D6m6?$|@ah;%Ym z$+&|38(Th_W6N|2k#ZH&S2Q@Tmq_{_yR^k$z+PD}8U-BnQr3=hv=fl1uS%ojw=n}> zVvN<|>Z|Pp)np@^t=#nixnY+5Z+FEm`zHlsE1(Ewm40#iClfu}^UD`wa7Z@M&=B{z zFNRk^eS;4_@+ciI#xJNe&`(pt5%uHH-IGVnapeO}0rWY<7&_+F08ofD0ZHN!7)5e> z)Ud>$SWg#9THwrhF#7AtRr$DGs3|GA)sh_Muh;CLqsxT{?U)OACdz0*$z4FUf^-uO zD8L3hKEocN8|^

{dvqfDu-&w*}H(V+A-3NxZyrwQ>K@G3xEx!)e#ZbE8E+mQfWE zcy#t9uDl1!TfX5F5&sk!0z!r8SSb>%qM(@C<{ZTU&zjKgCmd0$S?PGdoWZ+&9kXOu z+*cDF-j)b2sj~tn)byCe#;+^6@qa;O3YbGi8EX<&M~yaY$SP=oV)jO`5ie}>j#8QB z-_va*&9E{`iJFzRU^~UHo%PFf>s={b5on%q#0i{VM+(tX$K@DNOn8Uyi?q$%j?~*d z+y)PfRL#QXWwCitSKX}gclN%BG<9b;XYBQ3w9Pn${@2un+Dq8%(Tu)4H*;TnQRjbDxKbC_f{h^vnvGSris9^3U9R&oLG-uLpSq0v z^u;*3(tBI2I-GuN2>p!VPem7W6S|B+9H=w`!l6N!t_M@=_gv+M)1><)@aVq;uhC-E zr9;b5US8g+J~}RQKh5{oi`Qd_^mXtS*HHslupP>oxSPaYl$G2Q=64qxl6IzWuZt>1 zrd;Z1n;TEN@)^Z8TjtjnUvJ2W?&fffii!S|-NVxt<8HpwwDd64YBQi^P_Hq3Vpmad ztmE;%cTJ8MDtE}Awg=@BPt4$#t+ezu<%sp9RS)tU6&DIM(hysx!{FA~jL8pqm!5++ zPv!s?*-}gD{+QL><>Gf=pt-Z9B-3pvlg-=boiEYIoc$q`QrnqCwA3(<*s%^uz7ml2 z+}{-0268fLeD>kwEK0KkH|$OIe8ZJsCt}z3b*<>bZ+#%5?N`h_g~H4r{nK3|yFJYH zMx_XgFBU<$Ep+_H)k8vsQ7gKHLKkm&%&?tGtmZ>S&u_YCL zPrtX*;yfg8Z@n<$IW?wA+fRSn;O1ZqTP-ieH0vIiyiNaTs3vxIdAGH~fa|qQ)uz4m z+UG)?O4;l_t^VXF0$z{9hJGqjed0zbw3 z0Egn6idQ3z%h?|gRgK*opjQFp`O;kacB7X`(sheup^0S4afxcr3lc8`spc{So0pMR@2t%%Vt z8*PkFqnXytasV3Eb9cJp(Pto@OV@#ZOGEp7$*D_3NRfld0pGSqG{gwg9!aE?9qWn+b}n8=-DRv&{eIsBx@Ev`4U#K`FWFX& zd`pUqBy-`>)!wr13Iac139Kv)SJ+#=L9|)i!oAI7lgfF>X;^=K_!jyyr+X?uisZ6- zZNuq*G}dniXw*_8fp$G&>Z_=MHEw9(P8V2CYt@NTLs`_*zAY?zgkFO?jh8WlcRaxB(MRt5r8>rZ0ecwpv#S4Y%`WKS4 z6Gw9=CF+|19~s*BsTA?1qGxhfN*lWYu~h0kX&Jp4-iC}#to4&-2L4e2Z!+CrKGt)| zGn_kiY0BR=c~-S()qA(qPN8~NltacDlV$1rxkiyz--Op)+k zOG^A;TVe1l1~JEVt#g7!H1+#fTef=b6dUX2gP3Fn8JF%5z%WAG>7;X*EKz^)T5JF0 zholEU-;|QCNH@~r0jKDVeG#1J9IQYUlZjAT^AJfa&4!byj*?CITd?sn6P86SMX1Ss3GdwH+F4d`+Du$z zj(A05B4Xs|f?feLm8_ByrIV7Feeb1a-=HXbU@B6r>X_^!&EfQKSC9OjCNQH#TP7+zN^;ol;sYnZDX|ZIf zWZ%~m6?r0(weUnjvS-hh-+8Ov@60sQ=W`st-}m_a@%7KlAs(;gzOVbb&hxy^s|Ux_ zljcioyS8^R^lGx^nqY5(QrlglqN6jJUFNd+M&=<7dl)~K)(O@b-_}>nz)U=6(Ri7Q z*iWm2)I&W6BK3Yt12Se}EVOj#dmNE0CF8#`NEj=Gl*J-E9V*xBy{2Vx)7Itvj?j5I zhp@=qTCVO(0O8X$pm5gG48aQ`<6uUMPtkiFXf5N7nX1_H`NJ)@WqVhz#4Kie3mB;4 zuWoYY(%UQWV8uX(%QA`2eLndKkhF<*%1v~r<Q5_dAZL{F_>WA1?F#D{$n^iJQc{k@I%w`$}dLpiHcU0n^6ZoSiu@5T&P zspc#0EN-r;MtV#hT$6tl=u^li-WpFvn?ko$6dBiGbaj)x-YaaDGY+dJ+t+n5s?M^GTf~ zvw5E`1d(EXgU8N^u${Kzi5}B_i1bb)80Z;i3S;y6IR}NB&K^4|owCI`UvY?eAj=C` zVK?41lhIwbQ|GDiayN2x9+n+P3@E8Xs}A#lvCuo)RxW%`*>{QMYPws5c}8tyq(ILD zMEXLPKMdYDVRX8LYZtAB^2aY9UmbJ5qOz#?54pKKEb^QAuf>F$6g4lKa}M@YSJ#3Y z&D_q8%B1-4lu>Ma2E8W)WxVF0q7GKC8zP4D26FqWd}y|>bjp*kz(qRagQzaBWjIqi zl?tPE4}AgJsr=Igns-lseSR1)8Zis}W;dkb0%6#-%XMtZL9p*itwGRdD@hRa8AsB2 zmUdom2B zE`h93$ktD(_nX$a;^sWNy)Xj6s5N?3c6Ua7gdTCQuhA0p5rJmHhdc4((=D#s*ivf^ zgPyLd;gDHc7_SkJT(S4S1k@gkbAxSbH1v8ouWK(JPSE49hnz$&!|Gm$9la5!Hz>_x z4O9o(9+ttlsHmuf1jhIA-Uz6%*Jn-cK?p77b~(zRn7GBq>FH9nU;Q*mguk(5mGw}6 zmd3%fn3$7rzSess%N^ohT4#~doicpKpR~K!y5Q(W?p5aaO4LWhs^wWyCz7=V@2e+} zT~9|!MAhYXoUs;Lw_FE!D*=%_0C^vaz@Pv{S7h0<{x!>91Qk1&)Ffi>3(=aU!>|_6 zroEq58CWW>q{!z))cbRJFs;f(n4EaGJny`Rj&}r0f_+cmSx_x z6e|1eZKfBG&u^~bSLUz>w%g=RVtUtQ3(E%{4zvPjIYQH+%jkf0XVHCMJzy|+ME+E< zo~-6437AT{wzGJXZmy_sT@5v> zvv3wU`_S|TRY4SXyFhYE@B7+sH<8VGveJo(LsTfp_Y)=;2iv9_Ul8p9Z?wWL1k3aB zgTOOP;zC`>OpsMF<@j=Tg6&=q`B^dL0#_=`gr2rELd>UoLQ-^8ik!D1Rj73G8(PkM zSeXS?Lwm=q(LvBLu`oF&EKbb_d=HBGy~FRikfuU6)_p)qck>Vw>r? zwF{;nF81A|rO+{CnP@k1ts8o-`+0lU$?_!*G#<`cc#XF_4|Yx_UgIAbv!aoVAcul{ zQMmJDWn`%jbPIPVsO*t_6q5r4+u(ohP8cPnGyvjz@B(mcuOnu-|pA;)|xP`(D=^+9v3hOi%1n?KEeu_Ib#HNWkyi^dV zU4Nq-Q!2sn32M|lX)xf->nmL8*L!Mk%E&m`ibF6NnZVhHMSj_^(}aNX%BDGrVnat# z;<^!HKsFw@Um_JAXF|FmphcC*Nd9+$-lfB zuu@?xOZ7NLko=kh{&`)Ev=DQGb_F@1nzZe05Mz6Zf^Od`7ygw>NVSshT=y~dxqQ?VjdvB2*mSGQp*T>F$e6`glD67R+{z=2wv*rx-T|PN5 zUnOPyjaTndLnqDp7jdt$xhTAf;@3hU!x{nboShBliNG{n`!}|F`hX{V+a7?UCT@pM zi^~j32eNkBK4}a$FrjaLqs%b^tv9`V?ko)s!Jepf8dv$XoyHBHMj<)z z+EFIDqeB6FyCsq|j;+TeX_z7gdiziPydgk_1nnxn0ofn}YVBQ8SWX>woMqgiPtcO# z1))q&Qa)qoXo=A~@47I9(`VDA$~`Eift!8C(Ne{g$jf%E4oQbw-$mWCe0T`)(F~@H z*raS%>fqCb34HxYcYAnQ;WA=VS$Q8e>?=w&?~>RVGRs^&pGk8-HdHOYvh#T}7eqo8 z;I@u+Os&7`h(3EyJ_TQ@!k$bIg=N=T(|r}W%@iRb-Z&b0PWK5{Q8U*@Uf#-;wH|Ql z>I2P(>xSSvfky>>>Q6bF0|s^jwi|I~N%nC@>{cBP z!K=<82Vt}wpHUYBe4$95118A`+9Q}lH_^JqZQeaWSq{NI42RW5{3*H(A-~sp%d*zl>sH6cJIltEPCkvI-GK6LTen=UOwnyi zRPPAS2gM5@cp5{zmaPj~=c9Far4s?iasef*XA(U1_7N)6AxsA##mf^fqYztF(fAsq zl6k0iT$|?W?$6aHySSkm8mDgLb`oRdJ|yLBc_Z)1*;rJMV-Det21|d4v;8--Vw6lh z);JNL%WuwZi4W+#6!Z<0{_bUrtD3LROi4*WN>QGq1Z3+4;4cJ*bIdy+vuiDbD&RX9daFhy@!OGC;G3Y+T5V%>)X+2)@Hw|}KUBoC7Rk=(YiPSk=-Y@9&% zc`o)NPSL{JXBRG9K%kr+v)Gf}_HLdnjl4!=w0LvGdBMgJmk-kD(`8HNW;~hLEze0Z z0vPMgq{m8&*VrBR?R#&f*>t_8AR=LCqcvzK_OdM&Pgfx~F&)Ude$1uD zoQy{xo&*v$zwu)MbfwKVU$OuK?7?ti3{L@|W{vF~sha_C$O_MyDbECRHwU2QX)X%g z_%$vZV<>6{$@%m{WpVSeY}D4A$Kro)=4!yJ(NQ}Atx@00<5s|QerpU=8Gm7)d)RD| zY>hDfig%v6turb^_-?4ZyBV6nd{AgjLWd^II;BLvupGx!2*u%ke7#SO+}%K`j@FYb zQ<9`({ozdOZQk|rG(#H&X*aMMDoi#cZhbL{NE-Gd$SGU=|ts1$=9 zakdVgWj^>CO3KQWZs;4Bn3~L+8X>7E4!OQx)pelT8AE>Wh5-)jHZOO#3!0+@9d8)P zwXarDD-975wErAcegZ~<8J`kXd>l#v%tFPdh7r1 zA9)+?H4sz^kFKys0Ld=?`Hf*yp7V1{{3bvMO_+F<)k^l$6Fil#yrsLM+GkVRbWCb$ zX^j2KEk3%(JGIk-?C-Es{ko-0FS%u}TZTC7X%x!7w-Bf)qE~bWmzg$%+XOF7llb^(w%!+c5Z(04_;$@n zTy}44U)d1Dgxxf?h>Qlu&X%DBAioqUy&K>%-&r^+f(6VqtI*N+?2aiUlK>ID?ti(c zVvQL$<$yY20zo_EehP%PTpVitz=Qv2sU;o4lzj@N@Op_@9#DyRUIEB_FY2f|K(q#r zcF$#BGE!YNf#^WgW)LTUFmYojfvLs*6t!qcdA`|H*~yv)d1--5dhTecCD&}^1%%qm`qGCVrrnxX$VUvq zmK^;2kp80=Cb{Gi?s8|qRcicIs!lBq_Ll%B?+7|*y~|xnYUx4tRG}}|X3($Oe2oM^ zdhV&CbBtdD*{M8xLWlDTH3W@#RVo<2tUJa^rrl*Fl2eS_wFgEch32jlq@9wcEk}~T z?z;^`vKxpTd*sVn@xMF0euB#3{{u6Rkcmrpak$XNSV=FFI`-qXwnI2e15^&*`L#c5 z2sN>J#jDdeG^j4TepCh*K*R^{LwNY$blwl?AE=DZIyMy+a=uZh(W12QMeMP=w-ooi_!@N)uM!!V7Xi70C_9c86g#q zJpF{;)R*dl7fXO^-N_EMrp_4CV*d^8m8UV7yYd;6l?o!+Q4!#jk(^u=c9jGSkG9=ZN2 zLjBb!Ly3rL5d-=j#-(V+?dBLE)Qzx>@#&}nRV{N{046Q~q}mga2Dl&_YlO_0-R`v> z0FM=L3xNikgTVqzUv%@SXKOt7H1CMLPIbXq?tAYlyF>unNo?o7w7@{pCf4rWvIcfL zO(Ax58-jPDY0j(#3#APC!Za@WGGbUDtw=MoSfrfGQh$5veBDn{{G92?j>Ir+t}j@! zUxRxUnZ%Hf88KwqkdDHn1C78u?ay>S_g`05(*b0quoqQ!A5dAJaXMoq=3AY!6ALc?eGV2CYTEOJcep|A9E^vsf}*k6we2WwAtYa!Mxxj?*qc~ReKUy63_WE zOBhL+*}}=aic-@LU?~|OPpkf`5~Lg}@$?i9p7m;I*+Hf)-qwh2dATTz5dU2P-vYQ@ip$c2eElBZN&hU~5ow50IfpJ_Hj% zvh|7t2%{jdZ6(U%y3Yt&w_t76UaF10&wdQ&Ob|^pB$4qx zWjQhTb_H=K=j1nHeb;0+Mv@wcP*1eb$%Li*=>}BW5MVVoN8z+6xJUw&Kd!-ZtWf5Bg}qUn<_bHxUd z@Zj`B9Q0*>N2cGYynKbO+&CAM*Y|?0!E73dsNxn#gtWa3@3f9#UzW8X6hv3zr_Em(|kKlCI1^c8`pg9wsgZ# z9u-Y-e14gQv{q`N1C7M8%sh~{lTh3@_h=H9%JCyea_}o@lNWdsey^7n=}tSmeYHb> z`SUFYI7WcoUm8Cd;_&`vyE|MbJi9eG?7?ZzuipVE!4OrCW>o`ImIc4S5ZXE;w-HaC ztv#`g3v&$#NR=2cTSyyp#MVF8(3 z5pHb zMWP68Ig8M#W7{KflA((98SltMl;BDQkiA?0-Vp2M=}14mp4xjD3Lgwr@u#BJwy4X+ z345-4C>v*BWgmIKw7HUJYCdW+DINdW5n;$+e_o^~OJ7xll->~yJ4>2BBMn#N=32@+ z5>H0m-_r+hj~eN^fJJ(>>WOSX)TuY}IWglNdl7Xn3Qv#P6A1|# zaLGkbtL)pyM%nYfxCsTj^I*51(;MBsED#fS8*94Oy+EMe%r8UNs)(~MS(P)#ts8h2 zxg}gvB)KIln~#0B-=D&WgSnoYlsN3sI!DosCl4c_Y~#iH1^_L3PVcc>56*3Y9AJyg zzARiDQ856Ncq%;`Xm43zBkF&XX2IOP%Wti! zAn`NgXrtC5Vf0c|K4x%UL8pHAe|~Y+Z4}HYxZ*7V;;tE4CxW}pfg4m8jDxKx1gp!- z67)(lkP2|&aOErr;Vz7e7eNGa{ly!{&iKTvcNbuj(1i!KU~U)k*>?9YVDr7pbJr%hvT)uZE7+rrv@PJbq-OC-0L+bg15p_f%;7>Ilk!Mjb!e8 zPe~nLh4Iy!am^@3ik<;HOlYuQ8WtR1gmZWBhvzN|x#_CwCc1&t1b7%s4KuK-(%lNXOOaQ^1>RQaWKrp zf@H;&2PWhcrs?EivdO)D3BBS4Xoxh$7#d+N+yh+JL(?WvgdxO|k*~sG794^(nZ#;{ z!&U|!ngLuMfRD<)6(pNx6eL&RxDtO|MfKeceQ&RQ&c1|z3>mLZbhSmot!#-5`SqskSf+q4yPlMyQIU*2>iWPqf+%edyctiUQf|-&sE=O&D zLlJs;4Dzs>c7_baTe|SF3?&nQi2Wte(STHzuV)f^>z*W*j(UQ;33eqx44HoCBA?66 z&{+5h;H-}06+#nzE7;^`7HpaC!8AbBQK=xPa1T9pT?DtRZ!5WJjUbS2QW=TEqx*7y zmKP|Rdpe*MMX;(dk1rhL`RZ;tMl_WUhC;gsHIY29Rkd4c)&Q9AF)L!8DjEa#J&ZlF z-hqZ5WCkGD>;f1GgNr#1MYTpRLDP_gxb!+TcD~6OK`~GnC+2V&c&2~AUvqcv0T~bkWEa_STB@d3S^&X|c0%zG!3(>s!y+A%X#X2&F7?T(rz%VXW z+eY2#LfC!rHG+kFtJo!70j4X6!fSS=S<-I1E5PT6K9sMUw1*^f3UK9}se~Y^f;I+7 z6N2o0!XfZ>0QxJD9-BRi46>-ptRcg4E|)9;0!?P$kuA?&i)|dl0x%wLRI4h|kaWOd zJ>;aMSOJiNuE7;*qpb=BB)7^$P74Pe6HC-3xs%o7fO+2x33h4;oKS_eZxxIDJw?Q&1r zB6fP;t0p5>6hY>kK#i7$yTa~<@rpxv98$YPEsN|E{H|C$4{=N&VYYiTYwjIB@ zhTeyS@&!rV`Xyy8`^7V(xP5|zu%jv)AkJ9R6Ke~Ak#lxkrsWiU)&<32$Ik*2?pkS-dr4FdSBn3RR;vWHY&5HnX6qTg*-JZ(0k(6QF0WI)hs zZ;C@YVlYOol{^RzWh#Upej5-;%*Q7D+^FkRvyf&~Y$JIwEiM}$CYqR#T@UE{h+Ru3F{7ve+1*@yi43HvK)aU>6{HiM3fr9Xp;0hp zFKsTJWV_sF>J$!&ZD{bFhDgZs_>z2n1FtR=;}i*2?Ll$II+A=X3oFQ1F$ttJAq1;z zD`dGjb%1a8;gnfvI}6w+0$Mtv1WZ`1p*+Qi2(!0SECeiW-7w=pM_3`%x)&f~QgJ8C zC!IQvJ2(lC8C+hrp1z7oH6iuV&`!F`Y%X2c0gQf_sy%*@&#{IT3EYVjt$4!CyYZn) z2{RBFo+My*g$lgbpJEtYAXiMVEsS(grU6QhYlT})1~`&CS801t&OwxgD&dfh4m%SN z9QW;hGj{MF=v?UG)g3JcZ<1e#@<_+FT7_^g&xeg9#x-68G4j8JfDb1w-is}fu(%_J zJ%9jC^W(bwFVBDg=`m-5Vq$LB+u(wNTdd`ZvkGd1T;^ZZWBWL6%`|~)GOaZ2(&@-4P zJ^n54)6*?yDYL?6J#|Z%i@uQgTS|^+d0`2Bd<9D#1xyEoA3|T6@Q-s>{+U=e&_|`? zH5b^4=9!qHVWs2IW~S6YYheYs9u(Gd`;_R8oiKe0)Yqr-pa@^d+sG&Ntr$wVwRRH* zXYIJP;%C(j%U3L1vPc~yEE>yG2hDjN=vegk!X6r4h9?51I2nGH(j|Wd@cn(s-bLk5 z7LjRt?lEnRva56orAp38im)Z&>zmm0ki^oPavul z?SB#Y{tW{6>siPo(v@_Z&i#;oaBE2CAB(L44q%orw><4VKk^C)(ufdEOlRGhLv1I8 z{sZX$4PWAV1BZV)|1$*9x@T&j)S=DrU-i=8uKkWk0pKv_^>j)Xd{ZA-izyvv6zSSJ zLY>3<_TT$%Cxghf6h;<*qdhU8w>-r}MoJ;8F2k6vp^S^%ZOcfc)SZ-a7u#Q?G zVrNFA_81cK?-}`jUf(5bwaZXSUaYUV7ZGtBSt$ZvbV7nRlb*`i{_jP(e=8-w(iDIA zQV{XyB-c;Pvpg>X@TR!97;=un9kYM@?$AWiL}c@!6f`{g$9_*w{84d=pv~F!w|J?i zn4z-@f};bPgx{Ii{%e|vO?%FUZ0GWN=(bNxK`LDa&K_9ZB1Qb0`}Ql}#~OS`^6>%0 zI&7VPk0ba=dVLVwL1rnk%ktx+qFA;aFTikl9Hh9D{Ai>i-a9VDIS$H_M}XXJkP z4u9-F9=7;b^7TJrJHGrV-6xwF+40x!I^X~2(&XC$|64=D*I$8o3mJ&`eKqs5Uzw)o zrX%A&k#m3B%m4Qy|JqOT_3{6I@GO5&pZ@#f|Hj{ZT}|RTe_g}mAO1(A-rWw;{E%J) zq0qvSfGC2JxgbYS^d@@)Up&WWe6)WI4ui;xk7SnXEWwXsSje3%qcO1w7k>QX{C*_k z52TS+fYYvLj)Km&c?Tf~aF=gQtB>l|e!orK4f}y28=d&%MTi&tjs;L{o8#zap=F|( z^vgw=UYwDu1bT^DG))T)@Xv8~X9N^uuKuiZ++WA(Bwd@qM~2;mz6ukxdo}aW)>tsG zl4uFO^`x3c&-@#*fMuajVD?d;p5ly*4l0Psd?G>#;B|tyw(=@6eB_mdJ6~!0F$Qhb zlj`rJQW=Q!fPDvY#-JtU29hga3e=8quHucY;ktRQ8kUAon&J&x3T+?ol$g*%J;msA z6R62^FdCzfAr)Aws)O~~;@Wo+UNZZbk?2gJc86HZ|I9n!UQ1^G_>?WZC6)tqN>N!B zSj%FSZ`Yr&-DezpRVkc>kYaf6Ys+9Ap^5>@u&X_BeXtQ8i*Qo0ykW$S)Th|zBZb2r zreq}l?)L#i2lmON=&@fY8ggD$=%Ju_49+5e{T_7BIQNO_6j*X#1{mM>Sc1BvuUG8? z1)Zu<HHMrOM`y23~^(#1ONo1*z?@*K;<&Mqwj zs>AZb8-{fNRI=ueHT6KZQDx{D5$^k+o_M?MCarOCa@K6fC*MiL#Vs8{_j6O9(( z#_ceAvK>>ib!_>bmK%K%C>$N?>cKn_J(p>y zgxhBfCU_GutfPQ5{TKhFe|v@hB|bLXVn6OrjaLMc0Bmff;^+03fx#aXZoiY26Zsx3 z1F}w}gijJHX})#p$vlABz)p;W!(cu`FrggNlNQ;n*K8gklCawnhcspR_aGzfiC|HK zUj?}b5F0yWi6UpgJ!GoHs)CSl%TFi)7<0or8*dLZJK^k>rT5js zs&gmU2LiovI6x(*HxBJoztIp0Cs-B(oocZkg1kv?5F_UlrksC&~d%htf#j&Tb@FYv7l(?RhgJ%T|yI&Zt_! z1DQZo&l9LR@Dhx{|gk;ZBT)XRIem+%%VpJF>!L(y#yQvATlz+lYDl8KVSi)=gv4~2ik9iU|G3Wyo9ejxk}eoUfp^1z>)?I z2)_mGI#DvlnJC6S452W;cmX{TgBj{jo-S>VgGad#TQ*+9oo-O{R8)6DEI+-P zW!(!2uoZXkF#7#3&#k__)q#fF+hkT|;6MZJF;v>bEio!9GV?H+I-x* z(%e4-2N$TT?dEP+p-+ZilFqy@&%v}Dv4`$wi`|f~ZjA+)F^!N&*wj{^RuU5jqb+a@ zZ`}vDJW!s{7dn;?hc-h8LTD(2^d@(}TAb_)TPa|@0yFXehwC|g0C9Wp>n(r4Ez|w% zmJf@H6rg`-y9NCNU}K0ZFFN?i;47Y+b?%#doL=l|D{$fE*X78(f8P-Yzx;0)gD^xm z{hXC)pq=l`bwl~yoU4hGPkt^Q0_jot()PkphZ4FH-t}~xai_rrJbmd?ejZd5o<5JU6Q$uqGOMGerV36=_`+cSLATNa(@zfs1u3=Oy+xK`hK&w zNX0CWKMJHp9-&dA?^*0J^n-0uPlCQaQU@n;6Rh!9D=5GsFP6124XRX%D_eJsfw$k_evRU%>5fv33Y&deR ziFBxAuqB}hdY#)uNS{%PQ?6lMhItNXTOai0BJJucaYmu+*N#x>?XCSx)Z#cKPXuIz zZd*%h)cRr;LPuA#c}B9|!jFFitOr{#W1M7^MsA=Qw~)V8E%kMSIXyo| z+Ic%kcC-5`Gv?2w}gCx)Wx%#0UYGf{$+QW{g$)X zzq8z+vs@MjRQ#dWySPyqrnJ^rW;G*y~O%wN2yMaiTYjrtYYsCkb%2z|-^WoFd% z?jC$_qre%*cscqo6a#{5tPb6PqdNXdQ$_UF3ePwXB@6wR}CbJEcov~=Hc64;W z01a-Iu-aRz@Q$7+nDHf(YkS3uahrQFym&t8xs!Tt}h(UN9tjv|HIVdHlT$O%p?0= zOM1AGktmM3UMC|__BxXpyY|w2I+@s9L^OxL7-oFI;)A{ug4`D=08$TdE(UYKcfZY8 z1KG1uNeA><#W#NjZyJacAZ5Y8a0-z5-BrVW@QELH2a`ch$D>O87_5q?h0!NA=qv>~ zS@HhgfJ2~@MAb1pFA1~S9hCO?_~Rr_rc$x(e8z6y`QrPT`+4d+t$V8udZ@E z{lEFEkv)tQpps~9>Oh~^!W{EwSfNC2{`Lx;*4lOR4;~t^X zh@)Qg$I{5}%8b7*f@h5hf5KgD$vuyoC{z33 zKX;+HBLu>}fFCcMAMAmWz^;W5oD{;}+~C=jfM2pB z&!|O9=a>F+e&2;BO%q&3=ZcaT+k$J8%6=PmXp}g8%*7N{d1~HH`x7qqAahRePH|*9 z5*fHnf8sY8__5mJJZk*3;5%f(^xLdQ^^E>rN-Pl_%W+R>emNQG=&r?2IMF(Y=DeaP zPn7_+`1;q6&77rj{0~SY|L;fs zAAOb^H*Q3q(~XH0AV0#Ai}O_f*shP++pVRk9IKzl{eJn619N#+9NxS{u0~0FbR?^q zoRoL{uvWe-F}mlf({B$esJ%rumhSE#I>*cobwkm0S)bRRNn&EF^How;cn9o(c*9ST zNV<4=d7D{OREUKUbX(W8BOo%};{9dOZgQKkym{`D1bEFhaE&yf45?9My-RbSK{daO z%Gv;^uL646mP(?$6)Kd1W5PizFVeqB+R3(k*#oTyW?Y~H%8ef6qxY@Dk@~$y!>W%+ zYZ#ho2;|Umn}CBtl7)b+pxRDvkqUta1!ckXCmq}6=jNj&hOna~Jo;**Bs+0X0OM(n zihMuw##oqoKv-7*mhhckx)Tz3?*u!@zMm#9E#4kSCF<||ecWzd`kOjHPb+Lbf-0Bm zz>S5}FAwq>_l&1o1zB5y#)$xxiX$-!&2cu`9z$990}A5yNTB26;x6H10R5gsT5FYk z5P*-+*M=<7qVHqIv1C}Ar15)oL~;mv@4SPyH38=y0yk6Dg8_C3W^1D+`V73{9n17( zJhEE%eIRlVB=UUzGk;=66gwp6Xj;f8Mc9HBz7ws`hc-aI9TY|7GZ*XGMrC6bA0fr1 z{-zg@ay?m^Gx|QL`Ntpo3}*pAQa?kb7dABRAhV%m-Vlla@Q5aYT~;$luh z;I&2=y-F>i#DAH57bO8VAHfwqi=!LQ_}Tm|4;FgRfaWOk#G>~QxRq=sQF{}|sr>2& z`~$i{;P*0VUJ(zJm0&)23d`&@qZ%A(fNMB|U*pn|yZ06}orIOaX@Ji}K-VGyVMKC~ zC*w+iIPXp}5H=oC!y->Rc*%M5Ez!wG8V1gPy>m2@>p@s}D>y-peI5V*^2BwW8c_J_ z0}litlkX%JN{#x}r?7<2p5cBK6Eo&a5PX6Pn(TtU&k}eXcz{CvWdYg~NQgd>_8kvm zegg9`#t|~%xarw)?Nhr&4Z|UU6Nfqq3SbHRM^Ut|fWSDTZBQhy9ML+`?_`0;YE_O> zFY()gpgsG3L9DJoLo&b1VmuT(_7l1*uUk!4{SaE7BCei0)5p#fl{h^{ z>?l+dK}Yh`PA3OYy@;F5gJVLz&eNT_Jv!zXh7(=}i?>zOWpa7@txgbx1kE>FL_g+T zMh8~=0M=L!%EH>=IIPwYbV+#oXmvw*PVaIn29t*G$#@Eo;+?dH>dhxyBqY}7J>u}D z;3PbT@-1HiaBSOYb=zIGRthy(L^7VA>!Gk3gY@y~>}ABMh+7QHpfzxa_GKeJ_nL8% zu7wljflBT^_)LhC2`fYCz%NlHf=?Nc*|%i1kyy`$d%?aFwb&MrZxLN9{&Yum>?=9m zcz=EY%Z3TC057~dpl)L!x_iU=RM4v{#%Nd<$~?R74!S&ox}5Z%5Y~<5XS#N)^Y9!*~W_lM@+%ZUtWV}Db-d@DIrL4?Ae?Ozu ze#t6f>$;+Ce)Y-7kwU?!5ZK1b=Y{A%NlWM&M~Jnl>tY+@S8kT~>qr#_sPDJ(x@)Ux zLN?dLT|==RRG5swEf*@7fxW>!+FSVu0789Pfq%eX5N8btaPE6KK*$;WuTR`_`iZYp zf+z>*Mdzn2n!g|+A&1Nf!wqWZi4{tq$YaEoMl9a6;cc&Ri=^L$lc%Iq!zT#jJr&JW zDdiBo#(`~?HHPM|G=sp(Owu4rbPehcW9V1)#@M)uJ7YR|fCo!GNbnxydqDOZfccvq zr1VbPV~f3-5EoZ+Ny}NxqM{9THd8n0RvI(dJMoIRRgA4~v&P#u@*$IFxL*nEA&7rT zn47#f@7Y^GymywW2xOlVGp(-|25tt_e%Pd69ov) zm57BYp{CM-O0ZUN1;H?&zdi|UNXV?zk(PFC%&Bpl zjA2L(hps0+xsX*MCN?b;TiJ|^J~;@q$GYcMGpmRxwr^32BFcU!AexB{Q$7USyd(^RuG=pOb|+PI_4bAO0(dg` z^|A<)KK+$38a^Tj>jcN=C!}BcQeI44iv}4W8>*tFGlYP0KL6(8rk2~c9c$CnGcx2F zV@h_Kq0fORzfPqGCXOp`N)z)s@2!9hcmivhGfXNC0bD5aQy$M*ewGoNusDN=v(mQM zNgWjD$+i!dKH?Mf`51)jsb=XmC!s9CTn2gJiL$8)MK&#CkZ(<^ce>Hh@kZQru9@w> z;%DhBc~*o}vgY!}=qyDkfUe&0NMF?qSJ0$X{0NsG{cA;xl?!|Mbf!aR6Oys&~f&NT?3FESmu7(A4P?sq%3*lWvF zkHRssb{Kmik!;m*sFbdlE3WsXm(IZA;^BvkeqkL{j1fv5(6-Lscho$+Iz@MfmXC-9 zEsN;&CkC)96Gv~oi3_oB*fki}Ey6|oVP3Z3JS@$C;p3V#qG+$b6>u~k?jM`ITL*v< zc&rOXBva;zr#=MT6QQ&fW{fY_j6S||x!v|)Bd2yHT74Fc8FW&>s}UVISM|9ENZxcJ z$JbiGVq@zt5oY50IVP6peUf3Dtv-vdHI*Z*)Rsd4-mrW#5SyBg$o%9DB zG|w}*S1qYaL}V~?I*x9sH8xN_?+cF`?ymQ=0M=_#cLVzpjU$M@hJGE_yK0=n%@kte zQsgQe?gpr`LLF3@{)vc|8E`>^Y2K1SjfKD@<>Q)eAerdJ6sUFI>kET~~Qe zH;Z-z*EDkpuBD%Y{<0GvKKg}DtT%svb4jWheJ5AibI!MzNr#%j9f;B_rEhgL)rch< z?1OYB$waK5O%bgB!$;E3_Otlo`=xIj6-TN+ZvhmfBBdc=2myd6Ukp1SnYt}b;d~w;xyUwv5^dpi$#PU#xg6;4`ZPphWC-@*eU@(HS|6tJ;1+h#ZYp!|#3RyK^iF-R z)f}lz(l2SEujxaEOp_yK!sWV--A(jv8ycHorc!ttPQdjBvl-^+a2}LXBHf;*dhh^Po zbUN;vF7ZyCJzhO+Wy@dF=U7MjPsM8;X)QFVW*^tV+f#OYG?bI9-UA5E)NSrdUmg+% z7<=i*_{rjxaCSiPsAPD}^;{ML-raUTV;E*5e-G;ZNVGa4>xo9k%u~2ZM@h?)&*V-1 zzRRpjBS^nue;Fbstl74Y8o?wG5dW%?gcwI;EK*YljCV3RUXyxeu+}WKG^LwdVkp9| z2tvBFJffNe&SSH-IP7x?8z*e8W_1^ADds|OAUnMIj`Z#nZS_Aa3~Ba`%~A20*V1~LP?eF4FMk8( zFp;iUrV!lzN_!oiLCZtW=i4x2+tRZiL^p_;(-9vnl}WBQ;G{P5BQKz!bEo!m^zJji z$;=**&pn3U^GgOHrN^cf)nIxfw|Kgw(61(a>*pAs#!wXwIw8xEXT|yu#_t~(ncBbk z4Y7s#&9IZgj5PX?aUSgbQo2X)s=Ok-fH(=Z@lE0+2>#hg5Xr+^K-xG;!}j2{5On|^ zbu-*UWOaK3FVL%Ws@m^1f0%y9mPt_??0I-TQekN?BeW3_$Rw*#O7M-%;SY^ie}O~M zWZ`cY^-6fqUrgvdfg?ye&?2})$_W%7DWT1HZoo@>1TMKR+y0!tISPn@)22Z;OdBAD zF>B?8*L6n|VSZ0~RHnxz3wx`mK=Ph|nI_@V$sH7CH62?O<6ijF;P?t*P&v_~>Bl;h zg+r$u3bnvA46S=d+Gjjcd}D>tKaiDPNAhEDX99U(8yr@?h3p~!XW4^{#+rk8-+l@L zvVKvFsJN+!2k-jQG0iWdL9^n%CpWMSKcvB&>}1Wo#K|~vN#w*4Rn^E<_lf^B)E7i1 z2G=_{n>gV2J*0O=C-hwYtwz-+X#e}*LHCRCfi9?mYSi^Fzcd2eK>XrJIWVvKC@=+5 zog;I2gYE*~52yrITy{YYChhfcg;DULbALJ}>WzJFMhzvral0$#UXv0M_qof4=eSr8 z;E3zal?yv)*#qcuCIXF?aU2UVAV_7Kk!LbTD2rJf2ieko`89qP(yA9Ed^{gQpaFGg zj$+HEKSenzG>7(+^-^N%Oy6K&4f8n{k?&!Q%?y-M0RaDj$NNaU8F;V`f& z2h|#s7MEV5i5s;)zbqL9DO$tHgWI!?Fg_^Pm_O%2b+BhPQmwuMtkKL5Vd`k2dZ(wN z0?1)rIiQ@a$QhC@4B-G}`q2SOm~sMzhY27iVD7y-eE(re2qiM zPw03|X#KJE1j`eh;#KcgfZXCf3V0Gl*>kWER-s5HWf?6;^UY9}7pYtxRHvf~BT0Mn zDV1uRh&|{A#ZX_F>-OkU+V$3bHraL%S*_<>2RZ`YTk( z0EENqFrRD@m3|xerc=RBYxW(yxt((lL0;9cEfw{~smMXYYPbAPMwt|lIw7A2&Jk!mXwAKzpiEaB#A z95-6+`oM!So76EErWMnYS}Tk?83 z2YT%*+?AB?JDW&xNa{}Jo`t<81ebA*a61%CU|uk{weD43|u(wJnAY(G_Zz3 z*yzx3CTC*2NYQn%VHauf^5(lreEmP1B-YT=pR0F=GusQIp%h?>%v=Y~ivYb&pMkW| z$-M<$mC!AgPV541BoJrkIx;LQN$T0P7B15LgG3jD$*xnU1UG=kikg8th5bsPS0wC> zXlaLiutwb{$OW+gH88=0sd5M&l>sa8M<3B|*jU7X9ymwh_khBu2bI|-|L>7%I{!xs zf^Yl}J||Z3Fs&zkwP~E?z*0^VWSckgDXxbccUWE^&kFEx+@*%UgZd@s5yBY}s=XvQ z9qAykBDjc!N0+oVHh#SzLQ6d+eqg-%N~YO#?t3@QcxgO0vA^CUqsoPVp*N0{Ii$-t z1i#KR-}ZV7R|ZT#AShM1v2_i{JUY6|hi1GN{1_cWc!Mu(4^B4Hmw?|#L&s@RJTa~r zkU9BD{wt!K*}H+Ukh%cb(o;{uQ>`HvMEmWCvhp_2t}SIm`fynVTg86`FKoChaZ+g* z%BZht*;1r{w$Nly>qN=n`TU2FNy>KF&zJCwUaGf}M1CG`0aRvP`{}~iI29eh-S}%P zQe8swHDnvh;~&KDgDE3v>#*<9&T0DRN|7sWAz?s@CDF=7bj%rc`LpDtW-b0N^YVSw zt#|G;ejo!lHFg|!9@ho- z+l9VibPB?+vRNu@0T1Ui3r z9X?lYhm54RF>eam*XD%mqQ9-mAHUD%0R#_)TSNE)dnl^#JipzD>m1*|K^8yT9Ihb7 z*bzW1ff(Y{g-@a=GRZ8e?_WPk`#Tcb{q8ojW5XL2b_ir>@(*G1y%275eXZ)Ay_JM+E*47ybYJ$RA>N|F1pEAMdRcSAbg%BGV}#pKpPVibSSYVboj<2{!ATS;7Eg^CE^<*h7==3d#X z7f3)a>py=%8rx4z>%-W14-?==cl3_m!Ree04MLx0(DlZ@gL=&yTlxW(Z-{)A8(!^}nJE0Op zaxj<4DbUPMan-HS8_bOHd+v_36Pp!#qKIq<0j^|5!k%;hua?8amy1-mq`(U49|gwU zi@7P7=+h4Hs|O4hI^<8mzcm-c6D98)j>Q4|194xFog(I>#HyDcW03(Akp2&!*f%JZ zizJA1GU?txu`!PBUI$DCAwBj;+s*h}&@g;ptQXK&Q$3mW)AW0E9-5xRjjZ~^?;@`) zl7NVJx{LvbT9l2Vs2hFwqid=0F^)J;h&}ZBKxfDPHgFJw=riBrl5#=ChjjD z_byb#K0rC(dH1kK_)Uuw1G{h<{IHuM*Sm4Kfdi*vg8XuOo#5b+v>rzq;9MlSwN3pb z{UI8Dr>awU>)auJ<4YNHh(_dr{?s++k<*eY)eva-=G+YKSOm#MmGZt2xSA9#PmGE> zKi+B$gNXI3{3kZ$OJEF-1P-86iy(R%K+_I)-W9Vc@_0~m$O*4+yXH&<@sWfir9M0o zX}Ur<$C*{Q^dgr{A5ONEg#->| zK|Y^@KBN5V{hQT%?-g0T&GUS5{>3Pgu^g16et#erdi&xmdfc3c^*W73x09z#6)7J#!VX7oEFb7F-x@#-XOnjTAz_HJK8c0miWl9yZN;27mBJkqzj~4nEmX*re^Z?w z-6HpSB>&`oaj|*_$Q&lehKOwrE%E+^LEwC4b}vl8ZOD6IXu*ah%w%q)FnZ4hZzHi+ zQMqn0)x_kcjwZ<4jOZoP`k+(^K@4i9puLlai*)3BbVMUH`>h!WU&cz^-ZnvuEYJ}J zfJ>(ZUSh+fM@BSmnfx_k>%0qeu&Jw!?`{-x>MgTl?zT-35E#AFK6_7eQost-K=-hh zJ@7~wOCBD^a;@H+f>*mffN_T&KSH@^3mKpAHI^wg%m|7+4+$Y`;vo>7(6zwQZPPP| zs>!(e!PSt<@48JY zSss)a*(jz67DvELoF5G3WZKZQMgu?#sj2D1TWCGnN3fNRG!JJ1cOh*!3Ni9dmUu*@ z?FPu>E^wC8nZ&3Bw>Mu*3xhxKvD`+k&?<3rGqb7zHo?rWNbW0|V~#XP;zx=?31_NF zhH3z9!N?WEHC5P(q|uf2)2C)1Pht^I*+LwC`t$c&3_|DRubp}7^?DcITLh=|>$rIHtS=*;hP7tF1H`)m zYP=nc8_zMC#;2=(n#bwe=&V_l0~7;%Qhi^N^HA1G^EBm8rCp$hu(X?z^nb=#h9`Bk zDa~(H2Y|xmKH9;Y&q8!Daf9%*ejTi}D8qMZUmlbJPO(=xSEf=a;wM zPqN>usLTq?a?zFv@LTd@$Z)ybnC8x-%DT!IEtTJF2@#l-e!s>}-Te=+oG56S@eub~ zHSu;Xa`&!85IP7N;-f}ZZj~zz5a}6ob{aEOpU*ilB?QM zf|!{_Fd{gKU|3gb?;$dvw?G&qqAQ$S+Tj$-5@eiu`Qa-*=g_E|G00RUguB!wIu>4}tcu>(;N7GxPuw zd#_i18nA8+k^CW#F%NBQ2vdA`oy6~J!BptfXz0i7&+qf8CmkaLilj*=kdU^{ zbCwIMgLaxOdw73)JQd^B+QGrzV3>mw&fvqs>i%U%4!^!LT(ogK;bqjb^q4W3c(mxK zeK>_|X>a-6M5@AOc`M4hQ<1BOpoS_Xv!e`5L)MuLohp4&D%ERJ{jP)Ji5WB4BZASc zH$Q*`$}k>z&=F6agwjJ5>O`*UylYQ$uYKC$HYzXl1W{jY;PP|Z{hfON0*|!3*jr8l z!=9k3etk0Zg)M>%E?Od2)wiqzeJ`M?1oX@@hJ~ML_z>W~Fu9{@esxO`(}q!hA?=G& z)@^75Q-RvV5hS&=4VKq=srFqE_=(ZanVyamV(u$K*bgE|LF=5<5YaenLohcpE=sVr z^S%#}L2CG0(!yHKMYPfQDn2reE`6)ZcIxs0$+@gbu%Mf*&gVW6slfHB$QM8WvJSfd z_{~u|_zIK+Zb*=E%L_=2@~P&CpgX6=`yV4ETR4W6+g-Kb8|^{QmSx-{XQtBq1nl5M-+;%%)~w+Bmmfd`0lhMDa7#V4yYZ$9_nmL zBDq*?bQ)<$~QM;S8MqM09Hdz&6a-V(oA?doseS{}5ZO z%wGFfA!pf($#;L)H3&~K!$n2ZyP`hG+3afog8CLyD z50aQt;+BEJ)?l&1be^Zl=PF*&zQ_U7I5NGH0d>ifd{Vq#T}hz{8P+Olj6$fm*b(&v zsG-tT7IRMvga!z$T7Ny4V+1@ch+^WzHb|JFcj|nksSs)pgZ+REzdY;hLs`Tzg)oet zZ_&;@&3!4D=h#W9%$(rLyDXHUH0H+}WwX7?3rb2bqQ8tv{d49SbkR(+M?K?4*#nL}pGk$DbdaAF}}n+H4R7 zK!eW`Qf?pHb0L^kNs%chTkCUQGe2J!PmRCL(TA*mflC51Rv}fs>}Ev>3t;?t%BT<< z@#+HN*p(u7{p~H)6CIRH_$_|C4jw)sL+$Q+HA^uIJaTDI((2~TH%NLa#bGV%#ulp+ z#oP^zF`K$E@q`mLgQXSaSh&*75H@Z`19&vs1E4L6mvyagcgaR%%AYra_t_D8k3(?u z?$bZcb#UaW&uM)Dy974sGGVkFONSC4!FrAtWTI+qVH}PiE_Nsmpp7|y&vYAXZtg3F zg>?qMP_{kth-ji1T&kK=L?zG|Q}#3V^7#OLAL(IbPL;KduSYBHV*6q}FmvY&M^L77 zc!;6^f)iTEL3g=-Me1PiHNM8(LpAo}*&t$a_k!1APaKhNM8 z0UyQ0?us$L1<99C=z0)2rDcXoGWWH8nm*&u))s=Tn!k9=Ad$3|uWI09g>B#%nYQAy zLyA9!s`IClW**TL+GFcvu^6CBmvB{vdo|M)AZ&e8itof z80A)$v8CRTMi#QoYwRlMeKcDHv4FpFJ?{A)_Tu+&%w6$^Lw8Ax$o<`?(2r~G!LO(k6Zix zQb>O=R!sCx(IPZhZNXg|`;vFYv)j-t7Sux zw`AT;{>TPa5Zxau*lzCDyxBm-C8yC8erf=Hh-O4h!! z&VH*7i0`)^%0g4Zf|VVPxg4;Lh?fa zTXZu%I+i{`w6wFxEO1Jqg1qxh!i>K7Lgv)I4zowMgPU{*MN1k|%x9keswsNT(uDVZ z$6e%la2?PYK9>nJAY{K%6z%*b@8lPsr;+a-!|`sl})2lL76y zipmIJ&(w5KXqHS)|0P|LUA{q0R zPjHW}oRINTB1d!O@X04Dz>+W=*t1jBqNZ8!H>o73-jR$6k!0cp8FxX9!WXmp%YgG;Y z$((XX$=1xHlj6_(23=N9UaBZaL}xtdX{jOd77*47Izg@5NK^cpEkM?I0!UK0JKY19 zz%w(c$0Ek+7YcUVnI3SNmz9yrk|+7Avj0lnEKH97nq;52crACc=n&-d^qY*qQeDK9 zmuOK)@_@W=3@8Hg@=b5|$&4yhgeBchh>hLfc=k1%7T{02rG1hNCPTeSOzn@^U=$vy zcs^fo)wxcs-_0bk{<0KcV}H9FcVFPNvutaL<4r7fu7iGPCF*$0Ogm=VTC`Q_-GwFU zfXWtbL(l`Q7p_UGpw=G_B+~*L0Gs0EObsJJ{zXP=?D79(^`M0WmhIA{jw1+#-NSg4 zb5Z!oqB@ca7NJ&3V0<)Hy2ABx%D`z3m)W)P&Fo2Jbi$&7<3I?2s2}Q4%Cel2?sA(S zOeld?;We4|ShRA5gl!Ajh;HWQodjkV3jKHM#=*=tl!Bx49-W^B|-qX2o0%PH+e&Ko7LtwSdXjj{@Dg6NyL3 zEmL*n?t$yX=K^tcZZ~jD-Mgn!=zXs~pjHwVM0RmSTNIfvbFW;;n z-+`b!FaPF!j|K|~NP+ZD)p7NtzrcSab1%)tX3OISf{H`n(|nS8zF4Dp=;#R|-Op?h ztiiP|4G75bU^lmV3Q{1nUlGC7=2b%mxRJ-uV|V);DH|#lzaxdcL>YkvKPFeAcfcM- zGz1m%*CtQoAwius;Q5o09}C~0R7zvqj zMFzJXDtHU2vB&m}7m59B_v@__LeIFbi&Lr-X6>E^*D}X@`6_qL$dZr;O+&-UjTqcFJw0$Sh?Hwwaw}*wu+sx^g~G4 zhY?blR%YP&o%dV&xfs(s&-6#MOKj1qm%wf$Tc94>v|@1!GV>k)$fRcuR}`y#S!Bbu z^5{4iX(OB=n^7EXRur@XtL}H>#`p-FxR$b6)_BxFC)+DgEA@~`K76~pmM=S8M#kOO z`Md<^Q)##VxI_QzHgsc!qpHi#b#VeR@DWI`E@VLnwN9fHNWM%elpzfP-v8n@XYi#n zHPaI{f1bj{!*P=spncc=eDG<~LU@Bi7#)s5lEkXX-RS-eoZit@*A*{#mdxr0l}F4y zk0-ss6*mAk`1~Z%^>3aO31xf~Bu*awOyCnJ9EH~)Orox=c-2VCg&^di&gDHr`;(1{ zTc7ssuq8T)Fw(fN_Ya(zU*zqhW2y1N-sghQwL{^9 z+m@bW3W&aR8tdl%vvvEOf86l~5+Pz7fTWPW_ki{uF{L?2kk@K2?L= z#?%+rl^+;9Lx|3Rthc{RHB$9R_}$M$ZuR$Srdz@{Fp39aCx8aaSlQ?eJI?=x-Ir1gNA? zM*vw6R&mlvAUqjT`|_PIP)N=|Y1{m-Sq)Xg9fmR-l8z^!joF-^>E!@Y5)KdBIhh#* zREE$Iz{c`#diWNDH9o6ka$P6CfEFqWcw4{(6oxdiulwb!3HKy$HQ}Gjf&Vm4I2gg7 z2XP#vG=q@)AUO%yRX<6(iMSO)mqVKubafXBiT9=dcAd*V+=Yh9R%G&+d>0m%Kj90A z4|KrxK0IeF%_gjg*1wrJlvJcF1pf+xj1ZsFDpy};fYT*mt)7K}=fQv1Be2x+yXQl6 z-Vy6F@v)HAXx1|`@FW>R`d0kwztu&ax!->rjtpGnq^A`sP@ag}0D9Dq{o6h<&thqU z`f6;rw-Fm2I=MVyhFhv^JzC6n*ZzDVekT0Ebxu5ofIPv6r3u*o;sq1H3sQg9I`F-W zikzIf6h3_`L_rAfu-5iI`VCUXaDcY#jXgNaA=#I%O}3UR)1hkZy@5?UwP zNmm2;x+NxmZ|hBRM<1X5ADLJ5@o$U{RN_YFpY9Cw{%MNyH{TnX?0VxATBbHh?+X#W z?DAn*o!|W{>*eIG-fp|2BO@Q@Ylgv3dPd zO|#@iVeXjCW~RBhwI#!&*+I1fzD}3zq(A7^4$*RvD>>x$jxnA+-DQxVSvimefXrR? zFcb~m)SUd@$=@AQ@X6%yt;1VaOa@*&UHPe(GBrb);VtJ_Enm%`WDknGlZ$s+!{E51 z`wNJzl6zCs)zq3WDBE>3q)#}(QV{=s!gs=w#j=I6oAH}x-_+0OQ~p2jBgOV5&6t`H zV?yugM%dVVk#F>e4Mja9MeSLlLrT{60uVdnv<&tq;oeV1r zD>P}8_An3---5ou3Wl9j8#I&;He8++xrHW=gHgJVL6oK?Mm`7>X_?GSDwGNy7{#x8lx|D(f{lI{LD2u{o zKYE~`Zq}``D~N{i!Lsa=LksX(;!3hc_Qb&2owU+-z1?`ji$&G&ooTR7|2+j-oy6f;2}tzgf)D=hZwKhT znlC?yf;PM{0M0;dgDj9SnxSn4ini@`<3;|^G4w!=41EJpw0!({!upQP?3x4(8;m_d zI=gwFe&D)&OfH4!QWaz~GBoT#{^IUFr|#wLz4PoeR}Z|F?130jBU$$WMkgk)MPSg# z`{czj=MoRzpi2RFhyEik(#w=ssp&DPOLTXV9F{Rc64=OJE_#DUVgJ`+H zJgq3!6rD^3fmHZOWTBbCP+MsL)C-wxNcB!f-h)!zs{w7>TdiImt^%Q~GG39ax4ln< znM3>tQIqdMdpr=TZRjq3>~=05K+`64cn+X0ABF1TaueQM z+&bRqW52XAhxPLx-?x^^#!)FYPzFH81-VumQuoVNE^!@~y>0$1|$ z@?wKqHJs`hS@>cbPQo%(M;)gD7SpVPx|;kCZv#BT{x9km`I-7GQ44Y7_7A@DEMQiG#)dJk~OQxJrC6;~WOjwD?h5w>is3 z?URT~7TTSX$y-jg9aaku4XsZ%SGm^|FBizyKAB!N)uX&Y%EQBBlqUkxF^d54Zj&nx zSX`v?s@*8KtKHzAZy;0E9(d>K(S z&Q{9aQUckb5CxLs0FiFf2YB>UoQeAYYUY;INcd&RWaT*J=w%zV-Pa(CGk=vHCn2zD z#nF09S1HKP4>9{yNb3WI^f)hBQkg|DQd>7Z);OuYLM4z>fqgLzG!5~P6>G`1el0e! zW>7L&HTG}>b~|RkWr5>H8#l&Z-{vHPTf2aiz_sZ!urqh=Tdv(iQWX{uus+7RDz+Tf zWV~S=#ew{Yb?ofyIlx{JW=IEZyng6Q?vz$_xMxJan%QTI0b(*%FE{lq?r$wkaJZ<) z?|b7aI&3;3FXZLznfUD92J(Fh{C)<^2rU-nprJ&B$Yz`hg2)`Hvu?X*xN+esHm^bEf zWY3;FIQ&=yM3*)%<5f1sR0C=$E{s9fS+VIy>mirA5p&)bs~(M7IDu`Av~)w_et$Y) z*lcany4W@yIomLVJ=#9qTk<%FG+z#h+pJ^&RQy&E5nGtMQF{S!MS3%h0oxyHNXKx* zROQrI$iLV3B>@pThGS!6A$Mxe#{#L*F#kt1_k_kn zmv{}1SoHh37l5Kv6lFx^_>gL_p2B&&E^V((-xWxyjn6)sHT|rQ-l91(6HhXnSfC^L zcBA@&^)Wq%UWdC$CNrmERyPB~uOvtM;!n3OzkN~34p0HMScfPm^vxC`Y3KuWxpnu* zZ_^?-SaUj*-`*>GXyHegq{ay69tFKw$4#D>m20ak8V#qLxOd1WcjoM~qk7YvPT@O>S1UH{&vaEX2ZU7FO1n>?g^s6z>9D9op6Mka7CxzqR7jNF?3J zZJXy1%jxipD4_*?w`6U!4QE*w-$jq>lf=SlxCj=W|L2R~@UC4?t_d6EDc&Rb;~ceb zm)|fX2g2Bh7Y-3tHBeH0MIkCcE=(e{N{wQI4p?22UcAvFa9RJpiV1T+tel9)WY@Eueu6|*El z=;VMoR*#;Dwy2JWHlW&T$cQVb2=R4D|EAT0_G4!N1?|qs_KGxl*H^nh_`#hV?5a#`D$0fOc~5rn z`gXeEDbXZ*0At>TT$nL@wO@%K}q%6m4q=2k^s{6;!Ao?O_RclN$ zkyi@}4IKwbV&Oj#LLZ9Kr#xxO_o|LXFJw#EO`YERWzC)qF9*9C9~rFTA~sVNx5NiX z*A5Fyv^Fd(SI`{MCloL5i00)bOO_0skBW+d&oh}d2fOjKXvm&Mui!AElTjmuHDwT2 zp3+8TG{O%m1q&I7G2-O?QC}u+{T4i@0Qcc<_kBHwgzZ!+TMFvXq$=IAR^e1Na#$N1 zT-(xzuc66Qf8jC_C*8^U&D5^<6mN$l%~V4SVC=YNi1a3{z`;rP9pv%l8|Lss!3qeQ zD00yg>1DvD79aJ>#05XAn(AnXaE4p{OD1v^h-4E^9e7X4XODVx0kbe3!ITE)5ls(O zjp9YyaQR|*OlRZ;I3jk;M2GR6V_Rx!F!a)MwG#QaKQ_d!h+P*iK@*Ez zhcyP@!Yv|F!9%$CmX8RXo!x5jsvvURuRB8^P{7)G>7p!Wt7CE zAokQ{PATnbqRU1dC3^_kf6xkXnQczGJ4=pS#t=YHA0HnNk8<{VVRROu7_FZ>if@j?q{Ek)F6yYd5&!1LmF70X%o(tiwFS|igJMp@Uz9o=JrS?hr3av;s&x% zZFxQDz)de};_OV^H^m)4z6Bj~>Y)YMxtZXo;whkBMjtoReG>$a!f%zD;^?XttSL>^ z&}J3V*9G&GMQP5f1TlDPgy=+wrxr9kIa(ck9MIT;Q7SJlH{|kM17+F0V@m^H*|m$q zWyq)strbY8r1}o#wZPea;p9?psHSg9TVNSSufQ0fleWSM>d&exDY>I^z1nHU9%79w zP!hWioe$*KV4De?fzbA_0IZI>of+L_16aAxmMA3uJ?m6@NP zzhbi$h&&XOvJ}nYt!E**`GhM+4amglcGT}v%J6sI zO8;k|(QwkOgE2=MbP7obT1ZGSFJc7yQQ7;TH4;uVE;*q=Vvh@QP56fJ`RCv~TL-^_ zpR4I*Xhwh4-vkaOa`oyNdzRPYGc|nw_hf!A+sLP^HzK+7s#eNTgIgD-O2zdXq;O|! zm0#4~)ZARUgexD%;&{*>klkbYfBZU%*vBk8ejimpUiFvaZy7q_7avQJtMMv*!Ql&k zhF6BH$3Od#JwN=&(?9b3uRziwnNlXa;A+)6R9~{3Ti-4$cn_QIvu_K`r2e5~dx0G! zAHcKb%=MM{?H#dcxYCfgA6av`^Bwv1BlLg#_3tXaQ?&jpb}=g+|5r%uO4mloF4t)K zFW$g+uW;uB1vV}BmVdE89dv=hl?c7hi$?(CZ%nb9OVltOP^q`bVT; zIPnZhIOB$<``zJ8KY80u@|e^_9vlc@0h>n(Io#oGKvy+ zr{)w)z2orPdz}AK%6xpo#| zoq_Z>&_JB6f>E^9vfr4ecF6zqZXf77)uRelmrBMZzxLjoGiS+jzw7aw`X9W)+>;7# z=AHUUIB?R{Tu0jQt(t!J-nC9++1wEbS z(uE5bu3fu^RO$=5;tnewL0d#a&4oL7mg+07Ao&h?Q`;0d?1F(Fqv^o$xAH%%icwtU zvTJWwHVR!15#vH6_{il;k7}JlQj0^8j-GU6AS%qG5Zj&BX=c7`#M3~%w^jxH?Q2}L zqX8c^wKxN@cj$g);xkv@&}b|39#6DOghZcRpzl&MeaDNxU}}H&Sb*-{CAS_tz$Vl0 zH46c5P!R>;P?H1r=GLLNyUF-4yz?QqpzEL^)D9J9LmW~4OPoF}yL>xDM89d!puLk# zp_}#QIO@8*A`;!ikIIBYnp>$Bwf`!*tgf%S_ZGR^eT!<5RT~5jhI;E)kIZde?wQ$; zSQvncr?c`-0=!xcSw;5EiiGs*nvirpq#R~J)5IKy5bJmP{s?^i^UkbKM@4=|Ci%9R zb$8VTflkRDVrfqu%GBvAA3LMhpQW6g+Fa6(EK1@uqm-p(Oo5^J4*B(0 zCMR9-X9?ITn~GU@}A!w1p< z`EA9ARg@B(BXNyB)WKL@*i0I3O)oyT4XFWia9ieBcB7R=Oabv4-h5+RZP`GUbQ56x zFfJ)d(;lFRgAGX5mDsO4UA=nMq6|>hwKu$7RpR`W+%fsWC5%$OH#X{sRi8m+0a2zo z^K$i^Q`XkHAD5(P&`$Md6>_9NZ?wAYFQnq1ho6MnuNkn)z=#v416CI}hakunrBD!e zP;ps-k7_+zh3G5^XCCk}n*nna%xB?KZF`;B3lU$bFfV56>P1S6iK#CoW`UqA)V+a0 zqP(62Yg*E`Gw(OaWDSv<^a(5$d8pXuw(pCv>0D6d%a4^tQYe)7Td;K1r$LI{8isOT zF~duw?>T{InYE+#FH+-G*uPzL)QF3lTL=#V%E=^^7$1QwqH~4SI&dsqP+ciY^A04m zIzgJc*GpUg$U%~7oZm4g;L=j&VWgsqy+qH;YtzG#ZAS5$-$!8E2Lbf%+LT6AV8*F! z*TpG@AQ|@rJJ#cQt}qC@{=kSQ`UZUhgqd-wraHm5mBT+koXrvlL#P+*`JENqzCJ!R zr}O7|`1-CAG2Rd7f_xc%^+X>1XQ;vk*ZAGUhwlxx4^p*SNhQ2hTs9UmBJ9yiyY!tJ z=ab^)4>vZ9LK#{Z1QbX#<|VgUwD`1B7K8M~H7N#@pbp*b*U!rfsex?+sKc0v>B%pz zR7+yI#Fb<4ZZ*hu=oM@*D7S^Eu~4M1fuOjd3GtdsB{9seNhwb~Qb8uaX*sGvokPk! z9iJtEDxytCHN*X$#={t#yPDQ96fdWmA4+Pl z{66`a;Vt*?SKs%^Q{QXJ76}*{8dmWS4M_Gtm4!x?=*<0@kXiy#8ame$T0Rem{E&X1 zCN;(Lw5JG7DhY(!3k7fLEqo@A52ot6Y5;;kMZ_EK^bLgUkAf{hy_*KOENqM78Mw$> zjKENIP%w1|8u6HDTmoD!SwLA~esibW)7|}=nDzDR8|~K#Dt zSGVqhV|jKe)wQGBe5m_%E#B$sRO7rJ3%pUBgLPWRAI3nOzbGfcVo}QeDtO853Edkn zoP>|tX-5_Cjp#e$u`wGE4%Mp)Q9deay>&6aKz~-jB0SC7K}7V*J9l6yS&a z_Dcxe+si9KDe{uim8&2jqrF>*_86*AQ}bEyWZ!G#_)7BlE1Q?q9`sRgLf!|?KgNSN zjD&AUWJ1!{a?zt@F_mzAsot?p-XihV<+P|%X&I+1TCBicR_{L7UK}jc2=|DpC6$U# za5-39)Ns5(->4QJ@I{fpzQ9Zt75yEpFTu~wgm7E{hz!}n~B?Upy`rFml}#s`Rl z&L!TCnk36Eh5j6G7AfB`5U_1}JuMX2muY5LY2Dj?-`(9EQvDrL!V^-Z;WCfbAc z);<0JmE?3(0PJC7c-4|tDK@<)faL~PRDek$!gbKuXnmSMe_Mt0Q)t-Vqd#SI{kgI^ z%|QZMB7%ZtlINT^EFo`H=-$$2Yc_1yux;C`5%nq@a+L*J(xL#Sl$(}rl4oOQ&jFPR za;lw%#6`=c8bPtE$~_KIJ|@PE7@wdirwdY0h&c_iPsWLBsA&Lr_Xl64s|UtfHui)3?v2w_F;t4u69OLE_Ue9u__YEDpZGjuJgR`>oWscO`WJdc zMo{xt#4_EydwTddIX~Nc*%vJ$t+^v3BT&>j1JfSO)Wrzz0q`{zxAt;wwd)PO%2m1_ zdY~4qh_;v>^bXu{FbSZuJm15)c4FQ!Vhv#5h8tI7yQr;Lc`TzRs}QksM0QAqDnROk zgRidFbc85fn93Vf4sZxlPI;tajOEfu2rpu?^A2#8E$nm7>mz2%NW{>ZuZ3`w*h0C8 z?|W&iDJy~4qP<)8PK{9rePa00WYU5KOFHFy0a8_yR&Qcna%S{^z$ZwGyuzC}vf!Cn zbp#n%#kDjk0egJj ze?N$4uF>^?&N(_}HK-w9gJLHk6=0*?MMjpvX-Dy};>pZb*)v7kIzNGRMtb^D-4z#49@@~Kg*zPpx(bbb>+urIk?+{3ksR6~ zk`@KvP(?4y(&RwW(_3Pe=-ZE{f{6Zv7F@ao#2~l4nUFGsaG11Eklcp{1O!kGpW`eS zn^)ELQ7c*~m_&UBoJgIhdBu8~{52WZ+^)r<$amst*otXV{Paz4B$I8?_>-VNp{J6_ zoN|@kT^Zxsu-&QOnC5_Prub)TeQ!J%ex|^w5sKluq3EoU3gWh(nkY`yz~#1A=0F~& z$)H?~C~S6>Y3IcSM!nvo8o7OWlgXEC(LzCxnE+wXR@uScnvjwk@4!n?+j2u0Uw-Kg zWuX$0)ZUz4>1r;8BNLse1S?*IovAU@Rj1Opg%7K6<;pGQ68D#!)jaP&sa)+gALe19 zE`#6fOqv7ofq41QOlt5*o`xP6*@d0E2+JC>{ogOfr;rSXIr4?c^*N zGUoHeJPXUnw{VZGo`KNZ3DPNHxK-kl;pDYj%=G>8>=62!yGMU*I6XROK4H1<#uoG2*|=>&PCvRV(2hN} zU3@PtarEs*I(P&px^9gRqr1RnLM?|LD4G1S6XwBj&KXzf)xKd-u4dLi1oJJ>y*vmI z9>mY7cTZ+UYNm$MMav$8FKDOnQ3czb>My(7JzdU~g!+mw(`%M))UM*N3-sh={}A7I zoH)x;dr?{5naZ+W;jT2#JCAsk;w>LYF9nV86?h?6nasN?+L2-C^uEV>d;tZsi&{iw zl0bucHf~?IB>2fivGF>Y$z8!T)KzAo|Jep zv@iqnzSEy2yZX65HRM9{Ll10YE#=Nlv88O3__mofs2w)!YZop?#vn5kP;1&k8T!~r z(k2HCmOjA#;Q>lUwP4TZUzb?Qz9XQ4KLEO+At(#>-p5?w&BJH3j1OFd+@^yD>4~~t z?N;B{(ekX-rw_qRI4E*(IDN;tX+=dvab`5zC+?-X?4!ZD4{W7i<^Ui?ZhLXla<<-D z+&cFuoXwN95ose8q61k?>ksDiA_YiBhGS|@&2x+6Fbqv7sNK&#Nnbn^U3FgkQalaG z%Bx%aHOYDj|HG1cU+E;KThz92oR`Glz>PZU8IuT?)vuo_=f-audchBIfJK1E(^zU{ zD+SIjN#0*7i9o0}07+otoGnDKfDMe%FXEApNA4&FzTBJ^CAy!3FLSCdp&2aGQU)tKk1Bm141B!&^X3eTYB~0k@5fx!G_HYvM{S`-+l8ZDsu2rG zs24}{qZn$jR;KNHxzun=yBnDQ#B1c9FP!f9=mOL|YP~~ZzWkVaWQGVl8hdW~?2ak@ z0oWw^Qgno0XsCYV>MD^ZuF>f9VWrCxNHE%IRL-r)pW-Kyb9yRf>809ts$1*EOCE16 zQ@i*FoHsJ}ceIOa=-=_o@lC##GprL({9wJ24=kf~;aU#pHvct)^c_nLdP1S_ga-~l zwW=wllE)FihpsQ?lDo$8OTO!qtgwUNZI{Nq$Z@ct9l`g~2+Kmv`BEMAgs`f^qicP2 z9-VQiCjC#y|5waxPxDdI_s20F@RH?p0J`%kYj7Y-OM)yf0GQ}u{fCaie0*|4HSucl z_F665zwTZxmj`oE_xg9^a#Hq2xmfGw9vtcY@c?J7rYLTj&5RW$6JZ_XNW*;u9b5O2 zl~cvEmAzEc#4j!D4dy;{s%EV(zt1HrNtijKS0{1-BUoi5D6^|$(fGO3o<&H19BwBn zdQHQRE}4WPl2jwf#B>+VVc3n`ut+%^abB#=^Zm16%Vy>Gl+6ywd|#1Wtmb1yaeJYc zKkBO$N}JuBbfvtxmtrdP&UJZ9uZ1n&>B|X5cADJ zAE@*Z|L+92d55O)GzDNyEBvvYEu>1*0s7agMtaxuY|3XtsUo<`IDxsC<$ z{^>Q=kX2c&?04uK#)&T*9T09fpXp^?qE^nSB>lrcW-!}9O=!QI z9H`bJj3(m=i?K-eQIc!gVZOxe5JVvM5;y3j#lq=j$Hj2>4$1D_dsCe9gy*jGLs?D@ z$pfjc3zLE6s>dGQN$S4{wqsX&OX|l=u5z(>_4K+<<}~Q!Iy+ULhKphX(F0Zda@45SP4(yFbhDeTQCC zfV;16upTZ+;hNyEzq=lPm$Cnq$w96ux>oKeZ&3{745FVIwdwZQ1+IL3-k@F9>N zVkGD&i1MNHP=6q+JjfFDRdEO@YCMF?>lC;#gjIoM#=_bsS^9cwOa*=UmjYUX25Xtj zPyq@uz!9>m#$XE6HWMMfqk%StEZ=m-n^_=X)XZpjGk#h-&|2$dnFz3I_jZU^H;XzL9KUl!s#r4Bko5WMZFr_ zO=Mf%@7}#@+V~pS4wPd^tP=wbt`14|KzKUXEYN}!1QTa?bTSefc-n!{+?k5~X=VS0 zy{?pY&f4R#_%Cz~op~S&2`C!QE|s5#!UBYj5rl`Rtsas_`UcdJT8fp)nyF7lT3?|= zLCyu2xg8~&PPVpd8~0ShS$zi~JpUy!6S)R}&RXudbCNGo zr1bUIn6CAOQN#8LuZ{Li6QTvcctx!@D3iYQA=gnJ-y7Giy(H2@0lhDw^wL3cjy&xu zG#`07mrVsQthU-@j53gukRG+fftw+YNGRiGQg)DJd=?qVgo0SaOL_V1nte{Xzl~7a zF9Q#_4~$ISsLH;mIZWE+)d;8IXQ|>Q|9tn8{6HKlCPRiKlS#vO+jEuLI2G=Ra43Ch zXMe5H*LFS}O`Lu9)?Wnh8(v6!y955hv|{hG04Te0`Fx-`CW=AiwCl#0C&tfTvtI#G zUIS5|y+dxe;yqK~j&WPvt)Q%hQWkE$O24`3=o3^w@v3pRw_qvwWuPvq3o*nl#4AbEwDIx?)XFw0vHloC(66?c;1xY`Zzs5CFIRg8!arz{kdNP zJS2N?v7xE_P^x^JH$E=ORHcjUlAL4cV^ft8hg51v=TTjAD915at(qT7^9KA6b!fYH z?z3h_D1dT+SE)t|LSBRO$B9}UiYnti(MFY2`9@eSyyA#k3i%y}8wxhI$YgoWT7T12 z_pK8s-*3Ahia|Y9os;(w1q#uZ*_>SO%TH`#AyBEYM5ehFITrM@?cTTZm$!A3M0{_J z)BnU?jBobk7tqNlDf}Ewv_tb)1*>MuVMn*2%^4N1#A|F>V+x~kg9pkMTy6Lx4>K5+kV=#21mD@>Q* z2C)~7(G-$RF_v@^_U%}#xVv}pK27@xY+hkFy+YI?z)?K)bK6icg3+M)P_xn9&u2=n3GWU+ z%&pK&-0DccRaW*BmV1EA6=*YWE2~+WV-_B>hio6us3a0LQQ10)16@@-O~<^X)N@Um z7_f&_I3A}@p4+^=H;()LBi{`^)yVWDk-#WPKsyNX4i7F`it0z1KlJA5=)~d=cN_T2 zPS7+YlRFmg+uiGzH?U&c$yl-9-r~|LPR`5A14pOI;fb`(efT|?t+0TUs%kH?O2vkq zcJ|2ns3{{c4g4OPjS&T5wCf1RjXcjQo$``fC)Tsxcp%ZmxBUYV4OiPOTs6Z&`@i5a zzHh>i5cO>YItHL;2;I-P4rHSPfW;sTLBsxfc{BhCEpQL()~+r2jTg`26E=JpE^v+@ zea@|7VyBR%;~Z{!g=9e-oe_2q8|`E+clPxY28Rz6;AZ(>4rM#(u72w-V!rCZk|irwPGG{Wd*{un-P0f2&uykYe7I{p-WA^2+mA4{NjQ`O!~h*u z3uS=8$A>pyKX1(~-L?cYw$}f|l@bbj$PN|M4hI@kGl+N4{6E?z1$aeN{+9(#x-({@ z2H%g$3Tinkrhx{$^%57ce4$6T&AAh zk6lAQ`AFw&qUjL778HO#c--}oyXh?b4r)e`F%G}bP=Qsu#80+esk^B~Fb+GYnJc>H z8T+i_5$^;*(X?O`8Au7!A!p}@v5Nw!&B)%~-#87#lQuz}n~UyCQFWr?`1+8CzSkFF zt0jB)_y zK!A3YeFhbG!{wJwq!{FlC4l{DT2LIc@kSk`8VSj9Xyuh~evRjoDcn{vuywN*=^8Rcm0*fahR4zWeD?()owM zWTlgi1cjo0<=NX#$maDldc<#x;6@*RGAA8KYQY zY_Q$vRLo8Q8N`kcc`V-Y7VB5493A*Y7#`RmUvw~Tzc>wA8fc&K?)UzNMK6)|{eZ+5 z3GgzSH=C%)nC^$SLGv!m`5&I_xpn+5$jk{(<$6oLw(-lSOymP-gbDfe=CXRHdTg6# z=L~1vd)|y&+qcUauuNU>xog*!ORN{*>RQgqYNj3q%2@|coil7!*REY#MXB*HYC!sk zCV9K%%J<5erIAG3&P0-gbXRmt8J)apoFDO~ue2(OTsq?g0#BrhIw=D2~CIYBa>t8-hKOSu*6-Z4;3S~jl6xk>fJ{D zhgpuzoN&PEyN9Ncc)iec4$he{ zUBouOBzJHe&`X*=FUV#`#nNp^W5$T|i^&SZW-;wOFf=rg3IrkVw?wq23ls)zd<~-T ze);}X{yP(TJx8hxr+z%b zGKN?#WIATKgAKWJqFzphz?rrPx4~o}t%aXRi(_{-TK9I<7@wT+P^t{k#&5};)34?L zJ@lIiJ^I!$WhkWDF|+467)mXe2Yhx1)#QW+h<0bopgSA%Q?A`Z{O`ZX^4tmJR=<;J z0DS`Y!+U=xbg_5~{wn?sp_`Wv^^M`DUl^PV{vm~ViDb^WKzBer zV-K&XwyXr+PR8!W#`3suz9G1MZGz-+v=ic%GE|SbpbLy5fmzAV&ktzXW@;j0s7=Ge zIu5&PbXiqvl-yB^lwCl3xz;xad-C&QfOUPKG*{5jS_>hBOPx;kN%OuZ`SSjgB{+>x z#G0xbm~19pm1Tsu{E{3C8pUS-6LmKFcxbt(p&J?#mN(1gdVT_esv#!mY2V|fqXI(z1d+J03@{_O@%W9a`Yq#gxl5ijKh0%_$3 zfmX^Rz(eH9NpUN8&$Pkl-`2AU@ZuL3NV|jsj3It9k&->I0$=+Kz6C}nqwN976*gsM ziQi*pc+0Nv=4;|Ya@nt0GxDRaiS6TDZ0Taj+wVZ?01ink*MzScFvsgEn%Ib) z)$T{(A?ZMbb~oTeh9e7^D~R9#S&Q);kxIG~c($Xw9tSCG1lRAFlW-+^6U6660aXba zLPPF&NPsg7kV5QD9p`L9U@p>^^5qZ9a26trV`%_XGAb>PadEeGJwKq3lrlNa;YvjrwGeYk*<`1|5YlKR^M;A1h6^6Kzb5#o9PTbuCh4rLt9 z8~HwrWp7R0Y1Xf3Af)r^+zhfQ4uACs5kjHX3_q0x#iw+Mo|H zVVyLR0NR`Lqf3Sn(Ncb2`IBHxUISfsi+1M!h!i!_VQy1EE^LrZdcof%ICx|p6x1o_ zDmzN+L^R;r!w#S(6A<@N-k3h*K;FsexDL2KI1$SqX@;{l;{Q3W$A8O zG~87e={+J3D&u_#HR}_}^!U{Lx}krsEKXYPR1X-;b-a01+IwL5;?5&?CEoJI^k;R< z(;afk^S?S<7yYm2)W5LaaH>b5dc%cR#3eN4z=Ve*6fLsBql{|nI(a=}RMU?sX&q^y z1Y=ggYho5m(<9Js7mH0ud9=b4D3a^jirqE$*zU(}QZ>3mc-OKJ^a)3!6BrCIi#&|X{s#^=67$8;ndg@!7w z`VR7kWm=JLpH=hjLO4+{d|CU&Ko;VD4g^vyGGx*Yc!_J(Z2S7NNV3?F_hGapOvq zhv5MNF#`@822xeRqQ9kxItFdGan48V#ZJ9tz}a}+h>Eb_;0MY*+tAIybe4SJ;0wL6 z#}p49tSVNCgA9ZeY~G%Qlkhsy?lyoOxA6vX5N(TwA7w8aE?)bchY`F+1mJ1ScvMSr6uyjqZLZK^Z5aGcnApnKtNz~)l-4iNlGOpC}gBL}U+ z`=s%RJvFWl)R^CBx=Uuu8a0*UW!MO z2P2{PF9{Xay+R8uu8FL4de?M@kxTy&Oz#Q_3#URIj6@zahcsraD;>s(QCp2Z_}xNx z4@0CWVGL!ZsX>8p-d!kQOzCRGNUIX^)P15gaVq=7VDMmhq5!4|JQi{RJQ1@n$m$T& zIQmM|nnmO4rYsUFfRt5l|bI12Fz9O4{C z-C=7*MBJY)yK@P3hQkox-;VO*Tu2GxA3eKhvT!j`W({3O8~!zx6|qnxa9~{7P8?#9 zciIuVX5MR#KRJ-qk=nJ8Ibv3Zd8MovS|T(>_5F*Jmchp0OFzaZgzRlEqH;Pso5YZ$ zJ|(r!ll0wlXcs;%;pJORlUo$Pa)pVpVkcD7Gv}?;ni15yR(Y8_Bj2)-XS7(RzhZNmB-6P2~+-*O0N@9b+ z&6rG-+v=atlDvhN_dw_UYjoO)cjp^E1iWh&!2C!vH0D>)-yHBVL}U7a3^D=JBBui> ziS0pjBm*0mU5EA#rpQzxG%R;4ONfUaAdNU!l#f*$<@f}MfX#bFS+i-YxS4=2^oA|3 zSvJC)IXU#0^Ny72b>Lg)N1tVH8vG;U`ov3K4V2dU4Z@J#-432InNnaU!0|Li1 z8xyV#WN!O<_8Ahv6NdHm+U_Dsk3~#$%)Gzl<8k?N)xM~wjs+ip?hoY-nh5w>N_q{$ z3%hbvdx&T3k%L{3K8A{){$v$d&b2Nx8+Q>g#rg6%I#^#$L7~R2fp@^EUPIq$uMe+Q zTFbk16h~{oF4|spl(43503#_~_JkOreIS~rc5E4V6Ib<_OS653F_@nU3aO}!1mry2 z^e8yo-kvb*e{&swsx4&ih@@rFYdoP*E6%vj2S^v4W{q&Vjb)X+?3fG@2D` zM?nXqCs)lvemFblzV(37qi=4%~KV*0jxoxt}^#&bfU*Prfk;c0oU z%W3eKRV8Q(76KGl-kh?eB0_&FEOA51>kEF7u~d2?B~ObXNf{rj&>?soe1K!<|S`1ALr zt3u<7G&yS>lTfVl80n4m(O13C!*Pj@ifVh){=zl^musT#gET91sKpEoZ%e-mI@n6V zBnK6V$G6eh(w!O^QqpWXBCZHxOu-q@%Aj-%Oq~iJCkK({0sn4@4+!sr5ef$m91wZJPkO7V4t%mRGLQmx#JCQ1sC{lF>WTS}4u#s{ z5)HDezHCmG^PZWa#4w6GYPf%2N}mO%hVE_@LxOBzx~RYFX2ONwDxV+Vs^>h7sdY&~WMzGS!@6}HH^qA|(aHP|2KAZ8a#7cW^tx>Xh|hvCCb z(F%JI=r9MnB(d8OH7u>+rq5+Ko>&%nG<^ULkUZaE4Hp-ow9?)}Gn(QMv(ElF{Qr_s z=cJ{Ac*pHbvnwzVfm0A*ftItlic^G5BtL6A%E4t~v8C`pxPA9K=m z>z`2vIOdoh(}!jo%c|Hlv!jsk%v+DU+HeQ1EVK`-oqc1rb^chu(+g-*?0Dk|rf|hN zbn(RvA-`8oXXQEk7Rjr2@a_YVm)?ZXOFvSUks5a%QUCPX30hDtkS)v)xMp*j3nXY3 zIr2>${!0i;O?w^(ou}=N4#^%AuPth8Y0t~Uvv%KrZD^*P6kz7*wtMr3j6}iJzUzVM`9%45|W06A?nJ$7KPoas#QQ&hWOgq zx1UN3Me6+=T&&M zY4GqWhwDX>$?GENPN3Rt)x)J-0?xT=K|_hdb~#Nu#gXOnFV(S1%_t$XQz+6GLQJ=z zyKH%@^O3t(%@uk}+2JvCM70NYkZ^Li|I2J9j$^gSH-$Hpe%Qf`4xmp8I8vR&)Um~% z$|dUCS=dHdMsng+#X2;q{l*!getv6x9`$)@a}I-yH$3Mi;adSw15(Z^{dRm>Xcg{% z$_W*x?9$zmmxt-Fgrcwrz6(pV&wUpWVOWgY3?7>G%^H->hog$|RYa^#7qQ|NF4j3a z2@UWqPIvB|VzPG_{oNrLm(ysz+zP-Z6;+i>ZX5(IxN6v0P=-lk8UXf~52w^9osk~6EltRs0_pUqtE z%;e?e#VzGODL=Y}Mv$D55_Hp`_FO+?(xpxdJt0Q$&5cHXc*ha{dJDkE zsI*ON@UZUtZg@I%T^DM`d<2bVK{IEA;*Fy*QEPK#IgFC_UGf>gszRrf>}wYf^C@zR zzZEH!On&;=rV#o3=s~mWK^%pG@FDX|_kp{ramodW0Cw8fRUn06T*0BJh=74B{7?n0 z=^A_ZO*sy4WDle_t3L((ALJSAbasY!^9#327d@u)lzAdGDtd&wgR#kadCdP|@6E$< z-rBI?n{1gXGihRnkV=&1VH=8#LKJC`3@Oc;N86CnAW0-;N}3Ign$T=0X&#iiBbCbC zJoldKuC|c9?dLeY<9q*j`{Q{Io9e^+3>mHid+*_RUl%zNif^Tyq=ynOR-M=~@I zuTceTcdn=-WoSUr+?hdV_P`#1hr4ahA9CqlIk=jwI8sxfar@OZ)j8D;Ylo7ASVeQt zv}zAs8PHZj3D8!dke`=KjGsCvokP4gibmy}oOpDrC+aP$SkdX#dYB{xAYs}8O}8jOmRqBe!*ee zTueM(HS=n%qR@%ehPa#F!YebEro>*3XntYNK50ME9W@)4>bjU}^t;!DyR1A^s2Ojv z`J{7|dcjtKV*<>ExQNqee!_ZJD(jX8)bmQ8q!~Bb9WMpNNU|bj#V-X2#2o)25W{nE zK5qW)SOBDy?b0I1;w0fG_eiz$(Gj<}b!)@0VOkIBmRe|`CA@Vf+NO2X>*H}xA}s)J z+=n3h$?jU}vIRy9!24K%ryelNqFT?Qo5Q{tDRdmSeI9oADQ0kV38ni_J8nkM_8OX$ zgj1H3Ylrj>Nx_}f`{1Pp4z-c{y*pAuWZ^ESvC-y6ORZcPj2!benfZs*f%Ne}G#@7k z{K*#CAvQ;`{NX7+YERYw+m~1Nld@Tlh^M8ch5f03x+!7ZQXmPardk7{dH06QWn&BV zaFpnT5DO&^mg>G6ww(s`B((F<@22e{P{L-vReSEQ!K@%u^o^%0(*FBm%%kHwDKaUr zjIh>Ru7=B+EXR$HxdD+Dsm1FJD)ym6mNk1GVqqC5K4`iQi+a8O&u=7a4TfkbS9q_( zP?rNdGnuAOCt6Q5oZj=;xbpz8mhK9-EfITgV#yRe6otJyDsicXKfHy9XC^MqMleN>&}?HjX+uWJV26N| zTPRGXd+@;lyF@5%JG+hlJ^z?AopjtDzjs7NO_qHutENh&3+9w%^!>o@8L4o}%L{6mKN*f9JbUl#W$bCBci;hC1g#b+q zoBeX170!t}z7lX7iY!Ra7bMN3oSFRdcXiy^29Q#QiSpnc4m2LZibG;&IU@;((U$`p-%Sfg7XaV0lT#aki$(VG#bW3r%seT3AfRy5fis>+}E?%98AVdNLI6 zmupA|8q~pPdzxUkoN{LU=)(|j@$i5%1lkw~VzGG7{$cQEoR~;Cn6uRN+m9M>Wj=qr zW5PGd01}~P$(#PIj3@5xSGNebg1z|rYaDd>?l^S(PWKMJIl4oAN{B65MRx=vWuD5g z$N6HZ;M6#feq16373f!Mzg1+E{aZzP$kdagPfcUk^jzpCcmnw#cegT8!Hm}cn{sh> z-ik1r8GrhJ_$ZjKa(0Iv5E`ZjqJ^m3r@sFd*k! zyy#*k(<0J-E1U6U6gD0I&E|4(adB|)**A>%nGPpfFzaM$`Cwf7!(I0G5~M42X)weh zmnDyE^oUJ_a}U653zhbkDow6ghZ240){Nwzz_Rn|=r<&P8a?-uU|NFqKB3BZBNU>I z+#6;Lza;rLl*J5^W-Y=-0EfrM5+3v_07B|keh4LGw8)Av{?e4SPsjUQf8l9BD-kV% zlu6fcXR8)x*E2@0)z_{7x>=}=4p#lBl+H4}7bjYvEv$e^5$ViLJ_Y@3$vZ(#k)y8` zQlrlx;oi+v?GE1Ie&HF&TkB7=yTh?0Si7dCrhQf=KnT?PHG>0vBFEou;`{;tG8W{A z-#p+-z}oY8&a7%$2e87TV^;k_KrG^U7qd-g(7z`3=;DvTHj$?ed`EB98lOIgPD7&d zG5+PB;%K$hIV%7opfJ=nQr9v8%h1m?oKtf%dd0tpHXLASVs++x7Nyiv^!vC z+ISD2^c;Ngl5BbkL0uACtWWZA6vJKnMwZ*N9-mpAZaQLhKY^QchdVu4~LnaPKBM09a zQg8wcYrkdj=Q_G!)yruxd{eebO-(KAeU8W9;WB~Ws4WgWR)eN6w2hh72cr6Q{`}J= zZyE7biP225-WzjYpbT;{v%B7c4SL5R84VLzso*bpyg&@H%CdQEY;a>XaJp`#=JTwS zKwiM@3bhPA7(*pXO3v6-Ape%y|MBp0fp5*)L;cMgKo>1!oipc-`z#<#EAO!dqMf<& z{w{7aS%`DfOs*u=+_*1XSjRsd1;2_7l}atFEJ!;J+;(14$9xIRZC;Suv-lkL&n#3J zgn$mp2jw*~s9A=8rX2Bq(zPfK_{7+&mzpYR0Or+Y?#e@n*1Dvt3YjkCSRL0CKqhuo@=~!Kc5e(Vg+|2*6#>$-cOcR)G|X4K{eq!4j_0Xd6>@9%N%HP62YavM1#3{IkZA3_P%%1eEDYYML_-bEIfi*&h$uqL((h!Ub#5*dC_tPSKaCSned>@UqwY3WsHA_GrsAjIQXjU`Z zhq+aUZI-Wl1PZ{z#btNq{j|&_lNX+DvgIQ_AW+KYWr``q6P~2(s}~xjg*IM%8Oo(j zBkDpPugs);aN}XpCrWbL;&9s64TfcVa%W;rOo6j5XO}aN|IUQZ*;Iq7WGVOxHqCY* z{!3-QW4I|>2Afh_T*m36O#E$em~ncwY}u;wK1v=NuZBuC17&F_j^r;pjE*2j%GOPP z>@n;&!^@jo1>=BOfk1h8+2jk^_giYaq)Rr#RG{z*xQycaH{nrOd?~?+h0FZzi>$%6 zfT2Cdt{pp^(%TRUhSp3LC|p$BW6L}+s4r;d$yYD|fGt`jP<=`E=xk=nd#TaGWWCGQ z*7oGdsPFPXnm(rp?jvBtH_+k*_CN03aco8O#tOiJjuvqjAeK%(7+%uFo$m)MI=qwE z6qu(X`b7VAYTI<`d|u_x_{BFe_1lf&H=Y!MCjcbaw$so@NbKvOQwT8^1~zu%{>Pm_ zfkVv$L0dLC-6&Yl1m^$5T!5~}j<~MyzI3S-lCeb$khDZWhSsyHu0?VxdOm^b@g@{t zP!tBdz%m95kzBtmv|{Y_b;jwuM|1t(={Vq$LAYO;fMI$MaPpv%fqpXQ2xk}W*ENHx zkB{!yRpe<;nnjhFwqEG?ebsqi47@4_T@xthk{%fkdq1&8@c1>d&Vh&9&xr|eX9&&( z=bIjRlbOszqs1rzX1pxEk6lnc#Pu^k@Pd`gS@+YHgeZpEp;Kei%F<<(2Y|M^MgaZL zh&M^sYSD96%-5M=dM+>hrSs;8fZ3sYxv#E;!)1BxC2+VYD+vucChw-r8$EtW&_pP0 zc9}ctJ2*mmcq9w-OL+%w0B>A4YnE2x!!>oOe?|);3^KtlW~U8WqeD6;dlhI^-~j9$ zy1Fj&1G6B5RR#Elh3W{&L}6BDQ_{0%&(_yew5aCkSlo443YUe`y|)1xy|rG}CP;cf ztm8g_W(?<%pt4YDS(Vv*4UTx^dJKDjF707_$s?u5y1!}J_O*{- zS$8FmpMT|T-(H$sZ+*~<#0NuzT`9~qnxOD7@{WyXyC)TD zoK`)B%zO@i3d~dQ8V!S+loP{fcZSO74gVdAVhcNrAnD6OuX?uc(5F~cw8fk;=9q7O zzWJju7fceuRbHNkjv@0?E2c|9AtD#Kjj$n)qa>T*jNBa78`Rz!w*fSZYbRtKa1}Z6 z)NulQ8JeNWhSKVOK^)&}$!6T2u2l*JiP+9qE;MPY=@I6pUV69!T%QJAH0|;HEf4V8WnJ!wkK@9@Btmv+pcfsEcRa$ zk@}oD4+3z}#;8hE6s$?lVT9zykN)h<7xEf+1I_)gTG+z|#w73dw+bw`BA0Nex-SSQ z0SL^3+k=ooxJv{)?M3zreF9;I^Ps_ME;ln=shON8C~Nz>uFp%!W*p;!2~ZLgOJ7c(anz60@deNl6Et=1@@xcQUvC2U=gwE7RmEL_qjB+FuS z@@_-wzSD`7K1OjN5vHc7cg+{(3J@{Wh<nUhnfr>5(d^Pl$~1UG5u+%ntd^Y?3QYX zgMOuY>xL`}9~n&gB7vsaR%+fiwA7r=a@3fG*ZG1M&@wA$xYUc}qu085)IxlW>2+1`Qi;Zzl1yEnB$tKmk_pViS_T z-R04|{-P&!BYi#fOCH?HM@qrH^8h@Ipf(2hdIMb(*gnw(nMFCxoyVnPw4LcKzsyb~J~TwUBb=v{KEAth^{S3!c}Y&OE?kub zHTs)T8aRoYAddYhHSn}D&X~J{!9q!icb`}$<;?2;7C@x4sX5n0axHh&!_Ig<+f1|V zq-|;f;$|Ur&c|0zf|E|Y9Q-?se8H98T$ywr3I<90{Va7=NanYDx2rQqI-w`9Yr-YV zU5-ov_GAV}{8d`xppDua(6mZ{FbXh`W@AX}3lK9silD!lG&CI1hAI+;DLi9((OY@B z+MyTasDbN(mRbm<uG`HZExfL_>yXtLj2Bp-%TV;b#Js{Xq15VOJ zs2lkJ>5Q4|;?}JTi?y_zJ{fR|KvJ3!pmZH(o#HeIkCbiTB{K+1-K_r53^pjK+MdOh zvCWrH#+oC-gNnH4`+LV8PL=JRZZ!{N(2w9D;k{Bxjzcq2Ln`p<1xW)WlSn@cCS3B& zoCgVLSO;ADHg7n~6JWc0W&eVXsJS5HcfAe9LC&~}l2uMlE+ln3k{Z%s`+`?As>2_h zO|V`eo?H5t3tYp#;HuMOfV`m?u0VoGXxEdw&{v6c{Caw z{R=hR@fq7ZGTY+nvjb#?Am#Hu#NQhCSGO8g$2W6_fq=AvE;qHyes&ea>Ejxm|;^V9@4+km8Fj zKQ0$|7Q)96hyNw!FZHm2NW6&_y8(AyBZ^>j=5-A97#I!PVb^7OZsfRGnrxRKUYXmo zW;E=T;|_w_mbm4SX<>I(wBm06l^~{PZV|6Amv_Pve@Oq=>YLZe!Uhc;Qx_)p-c2KZWVo_t8LG+;i=@dKK%g- za_BmOsl?uexev)np&TDKud^T;$Q1|P?5`%;Gz_YM)Z?amY+H0%dI*69T)Lnmxh8G* zr1RRP%-`Pc{f`+XjNqjEzoeozqXr6N(;K|Gd8(|&fKvrrdZf7Q99b*1s&n)t6V)WV zyc^6e^j^+~d00-|5n|j(!|q2PFTCb@@Ptv%9G{`!Wyt_QaXbY+s(J1N8Yml?j*9P7z(7rD4V_jM0c{m3S=*lgAtD z4Ce5>B8D-xwlz$5?Po^5$SD?wkJ9$-uaGFCvR-~5Dwm8$Zm?{sRaut|`^5N%lie8D z$O&KhZx_=@N!ybIX#-{P-4Pv2@(b!T2)VZm$V8GFw!{_ckr;5qx;B$r;~qYISR>-% z>bl2?71JUb+vpe7uGiF%x35Ab`a&8K%40=j+g@HBe>~WpuznHHk$(-(1h^W-r%_jU zldcjhGR_3)YCw*H7RIm>Ly&phTS-jB>Jr;;*O-@jKWDTQ8Ve|* z^8xinFQ08S#8FGfnALQj8IMD_Z>84FFlIp%zv{0a#ofT@jp0DKJ-VVK}L%;e`>T(Z$s92-oHp-K#03WKz6#_xok|8_g7Jt+EP zruMh$?C)TSb#zp4tb+76J&s9&|M*5y`2@=!dkz1;H=ywhZ9;suHH!zK3!l5T4*dIZG( zoJv*d+WswVxggApB0**tz~+Ydb>lMH%0qL4BM1nTQjj;jpAH`L&y)ffgS&rI3p!vr zq~Rz@x`;KSKmY;)E=sr&=1L1evXiB>O$oARm~Og(cj0My!IA_-xJLU0C?nDtI?8j) zI({h_K7(IFJGS0aPE;%Qz)t}x9rcU#1z?_w2|fYy3sAs=n4Yf=D4#HTeYW&jI{~(N zK)&kjV)wC9_YSoT4aoFNe(MG74om=&Go=95Ly#-ZMauKGA_D0(I^9hE z3!RMR-`7Z4k1qX_kcGRgD@p@{(VqS6CDKx)g8&X)BJff=>X*Rx!Qt+rfHm}+NiFkv zwG&@tQ9B!p4cqw`CeP3TCwBPo-Ou#CV7sHAtiXGv`gvq^6grV$OnzjSTQ!WcJ>Tm? z&Ms$JUdxu_@ULcA%HI&*0(O zj8Ip}X5#y9hFllDQu&WaVTDAmaPk>nV$sCuMkbP=m2miH@l4C{KLvhEFlTQt=4q3T zsU(21EfA8H!42Spaz;DSK&KlZ-R{OfyZomRl(S0ud*EE<9B&=Ui|u%_v57S;3~A$i zKHzVpKcN#+$_vyxW?KOQM*_>oaXPYRkwSnpABzgeKy+AMyttT=bPvS9Wnfp#o>1i- z$)C_mv;4prRD!DP+RtAc^MtEODD1I9nn0q?aRZ6t=^za6+?q6A0C((bfG?mYE-h`- zF15s{O3y#7>#9*NRoL~0i7G~!euhoL?N$Kq1sOulBCB(3lY}{H!U;#l?t+`^likRg zP$o@Od^dcZ{GBA=UYL;BL@4^4y%_Ai;Q|kgANFu8-i|gU8)hjEai zsVF8M{ho>3cyEiTDl%mDHHWiimbN0ZTjZ@|1F($GJuj|re?N*@lMGMDb_>mP>_<9Aegy5hhtO)ABU)J$bzBJ(hnnN-IMJ->$)m_=wjD?= znl;y%T!crX9iX`n0j4PNf#3ki4Y8^~8KWz3w*hTfpoe#N3xG8UklwQk1Q3ZHrUpHz zP~0p7S0OKktBu3kmsHYp7IS^IKDS378r_sJDq(PcKGIx_m(!awu+-!FsoV{(sqU=n z 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 CtXY5n literal 0 HcmV?d00001 From 10906cd457182429761ae97567116a53f93903b3 Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Wed, 18 Sep 2024 01:05:08 -0700 Subject: [PATCH 02/32] ABFS Backoff metrics sample usecase implementation --- .../AbfsBackoffMetricsUsingIOStatistics.java | 60 ++++++++++++++ .../hadoop/fs/azurebfs/AbfsCountersImpl.java | 15 ++-- .../fs/azurebfs/InputStreamStatistics.java | 75 ------------------ .../azurebfs/constants/MetricsConstants.java | 6 ++ .../enums/AbfsBackoffMetricsEnum.java | 37 +++++++++ .../fs/azurebfs/services/AbfsCounters.java | 3 + .../AbstractAbfsStatisticsSource.java | 41 ++++++++++ .../AbstractAbfsStatisticsStore.java | 59 -------------- .../src/test/resources/readVectored.plantuml | 28 ------- .../src/test/resources/readVectored.png | Bin 211653 -> 0 bytes 10 files changed, 155 insertions(+), 169 deletions(-) create mode 100644 hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsBackoffMetricsUsingIOStatistics.java delete mode 100644 hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/InputStreamStatistics.java create mode 100644 hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java create mode 100644 hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java create mode 100644 hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java delete mode 100644 hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsStore.java delete mode 100644 hadoop-tools/hadoop-azure/src/test/resources/readVectored.plantuml delete mode 100644 hadoop-tools/hadoop-azure/src/test/resources/readVectored.png diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsBackoffMetricsUsingIOStatistics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsBackoffMetricsUsingIOStatistics.java new file mode 100644 index 0000000000000..be3df897ab4d1 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsBackoffMetricsUsingIOStatistics.java @@ -0,0 +1,60 @@ +package org.apache.hadoop.fs.azurebfs; + +import org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum; +import org.apache.hadoop.fs.azurebfs.statistics.AbstractAbfsStatisticsSource; +import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.RETRY; +import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.iostatisticsStore; + +public class AbfsBackoffMetricsUsingIOStatistics extends AbstractAbfsStatisticsSource { + private final List retryCountList = Arrays.asList("1", "2", "3", "4", "5_15", "15_25", "25AndAbove"); + + public AbfsBackoffMetricsUsingIOStatistics() { + IOStatisticsStore ioStatisticsStore = iostatisticsStore() + .withCounters(getCountersName()) + .build(); + setIOStatistics(ioStatisticsStore); + } + + private String[] getCountersName() { + return Arrays.stream(AbfsBackoffMetricsEnum.values()) + .flatMap(backoffMetricsEnum -> { + if (RETRY.equals(backoffMetricsEnum.getType())) { + return retryCountList.stream() + .map(retryCount -> retryCount + ":" + backoffMetricsEnum.getName()); + } else { + return Stream.of(backoffMetricsEnum.getName()); + } + }) + .toArray(String[]::new); + } + + public void incrementCounter(AbfsBackoffMetricsEnum metric, String retryCount, long value) { + incCounter(retryCount + ":" + metric.getName(), value); + } + + public void incrementCounter(AbfsBackoffMetricsEnum metric, String retryCount) { + incrementCounter(metric, retryCount, 1); + } + + public void incrementCounter(AbfsBackoffMetricsEnum metric, long value) { + incCounter(metric.getName(), value); + } + + public void incrementCounter(AbfsBackoffMetricsEnum metric) { + incrementCounter(metric, 1); + } + + public Long lookupCounterValue(AbfsBackoffMetricsEnum metric, String retryCount) { + return lookupCounterValue(retryCount + ":" + metric.getName()); + } + + public Long lookupCounterValue(AbfsBackoffMetricsEnum metric) { + return lookupCounterValue(metric.getName()); + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java index 39661b085981c..c56b9918ee7c4 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java @@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicLong; import org.apache.hadoop.classification.VisibleForTesting; +import org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum; import org.apache.hadoop.fs.azurebfs.services.AbfsCounters; import org.apache.hadoop.fs.azurebfs.services.AbfsReadFooterMetrics; import org.apache.hadoop.fs.azurebfs.utils.MetricFormat; @@ -101,7 +102,7 @@ public class AbfsCountersImpl implements AbfsCounters { private AbfsBackoffMetrics abfsBackoffMetrics = null; - private InputStreamStatistics inputStreamStatistics = null; + private AbfsBackoffMetricsUsingIOStatistics abfsBackoffMetricsUsingIOStatistics = null; private AbfsReadFooterMetrics abfsReadFooterMetrics = null; @@ -172,10 +173,9 @@ public void initializeMetrics(MetricFormat metricFormat) { switch (metricFormat) { case INTERNAL_BACKOFF_METRIC_FORMAT: abfsBackoffMetrics = new AbfsBackoffMetrics(); - inputStreamStatistics = new InputStreamStatistics(); - inputStreamStatistics.increment("TEST1"); - inputStreamStatistics.increment("RETRY_ONE", "TEST3", 2); - System.out.println(inputStreamStatistics.toString()); + abfsBackoffMetricsUsingIOStatistics = new AbfsBackoffMetricsUsingIOStatistics(); + abfsBackoffMetricsUsingIOStatistics.incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED, "1"); + System.out.println(abfsBackoffMetricsUsingIOStatistics.toString()); break; case INTERNAL_FOOTER_METRIC_FORMAT: abfsReadFooterMetrics = new AbfsReadFooterMetrics(); @@ -259,8 +259,9 @@ public AbfsBackoffMetrics getAbfsBackoffMetrics() { return abfsBackoffMetrics != null ? abfsBackoffMetrics : null; } - public InputStreamStatistics getInputStreamStatistics() { - return inputStreamStatistics; + @Override + public AbfsBackoffMetricsUsingIOStatistics getAbfsBackoffMetricsUsingIOStatistics() { + return abfsBackoffMetricsUsingIOStatistics; } @Override diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/InputStreamStatistics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/InputStreamStatistics.java deleted file mode 100644 index d9e658c02aa6b..0000000000000 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/InputStreamStatistics.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.apache.hadoop.fs.azurebfs; - -import org.apache.hadoop.fs.azurebfs.statistics.AbstractAbfsStatisticsStore; -import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.iostatisticsStore; - -public class InputStreamStatistics extends AbstractAbfsStatisticsStore { - public InputStreamStatistics() { - IOStatisticsStore st = iostatisticsStore() - .withCounters("TEST1", "TEST2") - .build(); - setIOStatistics(st); - - IOStatisticsStore retrySt = iostatisticsStore() - .withCounters("TEST3", "TEST4") - .build(); - setIOStatistics("RETRY_ONE", retrySt); - } - - public InputStreamStatistics(Map> map) { - for(Map.Entry> entry : map.entrySet()) { - IOStatisticsStore st = iostatisticsStore() - .withCounters(entry.getValue().toArray(new String[0])) - .build(); - setIOStatistics(entry.getKey(), st); - } - } - - public IOStatisticsStore localIOStatistics() { - return InputStreamStatistics.super.getIOStatistics(); - } - - public IOStatisticsStore localIOStatistics(String key) { - return InputStreamStatistics.super.getIOStatistics(key); - } - - public Map localIOStatisticsMap() { - return InputStreamStatistics.super.getIOStatisticsMap(); - } - - public long increment(String name) { - return increment(name, 1); - } - - public long increment(String name, long value) { - return incCounter(name, value); - } - - public long increment(String key, String name) { - return incCounter(key, name); - } - - public long increment(String key, String name, long value) { - return incCounter(key, name, value); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("{"); - - String result = localIOStatisticsMap().keySet().stream() - .map(key -> "\"" + key + "\": " + statisticsToString(key)) - .collect(Collectors.joining(", ")); - - sb.append(result); - sb.append('}'); - return sb.toString(); - } -} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java new file mode 100644 index 0000000000000..be0e2981a39f4 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java @@ -0,0 +1,6 @@ +package org.apache.hadoop.fs.azurebfs.constants; + +public final class MetricsConstants { + public static final String RETRY = "RETRY"; + public static final String BASE = "BASE"; +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java new file mode 100644 index 0000000000000..c24ec273efff5 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java @@ -0,0 +1,37 @@ +package org.apache.hadoop.fs.azurebfs.enums; + +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.BASE; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.RETRY; + +public enum AbfsBackoffMetricsEnum { + NUMBER_OF_REQUESTS_SUCCEEDED("numberOfRequestsSucceeded", RETRY,"Number of requests succeeded", "xyz"), + NUMBER_OF_IOPS_THROTTLED_REQUESTS("numberOfIOPSThrottledRequests", BASE, "Number of IOPS throttled requests", "abc"),; + + private final String name; + private final String type; + private final String description; + private final String code; + + AbfsBackoffMetricsEnum(String name, String type, String description, String code) { + this.name = name; + this.type = type; + this.description = description; + this.code = code; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public String getDescription() { + return description; + } + + public String getCode() { + return code; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsCounters.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsCounters.java index 65e5fa29a138b..f490013b0668c 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsCounters.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsCounters.java @@ -26,6 +26,7 @@ import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; import org.apache.hadoop.fs.azurebfs.AbfsBackoffMetrics; +import org.apache.hadoop.fs.azurebfs.AbfsBackoffMetricsUsingIOStatistics; import org.apache.hadoop.fs.azurebfs.AbfsStatistic; import org.apache.hadoop.fs.azurebfs.utils.MetricFormat; import org.apache.hadoop.fs.statistics.DurationTracker; @@ -82,6 +83,8 @@ String formString(String prefix, String separator, String suffix, AbfsBackoffMetrics getAbfsBackoffMetrics(); + AbfsBackoffMetricsUsingIOStatistics getAbfsBackoffMetricsUsingIOStatistics(); + AbfsReadFooterMetrics getAbfsReadFooterMetrics(); AtomicLong getLastExecutionTime(); diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java new file mode 100644 index 0000000000000..69ed09ace8a5d --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java @@ -0,0 +1,41 @@ +package org.apache.hadoop.fs.azurebfs.statistics; + +import org.apache.hadoop.fs.statistics.IOStatisticsSource; +import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; + +public abstract class AbstractAbfsStatisticsSource implements IOStatisticsSource { + private IOStatisticsStore ioStatistics; + + protected AbstractAbfsStatisticsSource() { + } + + @Override + public IOStatisticsStore getIOStatistics() { + return ioStatistics; + } + + protected void setIOStatistics(final IOStatisticsStore ioStatistics) { + this.ioStatistics = ioStatistics; + } + + public long incCounter(String name) { + return incCounter(name, 1); + } + + public long incCounter(String name, long value) { + return ioStatistics.incrementCounter(name, value); + } + + public Long lookupCounterValue(final String name) { + return ioStatistics.counters().get(name); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder( + "AbstractAbfsStatisticsStore{"); + sb.append(ioStatistics); + sb.append('}'); + return sb.toString(); + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsStore.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsStore.java deleted file mode 100644 index 2c658abfeb563..0000000000000 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsStore.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.apache.hadoop.fs.azurebfs.statistics; - -import org.apache.hadoop.fs.statistics.IOStatisticsSource; -import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -public abstract class AbstractAbfsStatisticsStore implements IOStatisticsSource { - private static final String BASE_KEY = "BASE"; - private final Map ioStatisticsMap = new ConcurrentHashMap<>(); - - protected AbstractAbfsStatisticsStore() { - } - - @Override - public IOStatisticsStore getIOStatistics() { - return getIOStatistics(BASE_KEY); - } - - protected IOStatisticsStore getIOStatistics(String key) { - return ioStatisticsMap.getOrDefault(key, null); - } - - protected Map getIOStatisticsMap() { - return ioStatisticsMap; - } - - protected void setIOStatistics(final IOStatisticsStore statistics) { - setIOStatistics(BASE_KEY, statistics); - } - - protected void setIOStatistics(String key, final IOStatisticsStore statistics) { - ioStatisticsMap.put(key, statistics); - } - - protected long incCounter(String name, long value) { - return incCounter(BASE_KEY, name, value); - } - - protected long incCounter(String key, String name) { - return incCounter(key, name, 1); - } - - protected long incCounter(String key, String name, long value) { - return ioStatisticsMap.getOrDefault(key, null) == null ? 0L : ioStatisticsMap.get(key).incrementCounter(name, value); - } - - protected String statisticsToString(String key) { - IOStatisticsStore ioStatistics = ioStatisticsMap.getOrDefault(key, null); - - if (ioStatistics == null || ioStatistics.counters().isEmpty()) { return null; } - - return ioStatistics.counters().entrySet().stream() - .map(entry -> "\"" + entry.getKey() + "\": " + entry.getValue()) - .collect(Collectors.joining(", ", "{", "}")); - } -} diff --git a/hadoop-tools/hadoop-azure/src/test/resources/readVectored.plantuml b/hadoop-tools/hadoop-azure/src/test/resources/readVectored.plantuml deleted file mode 100644 index 57d17d545cfc6..0000000000000 --- a/hadoop-tools/hadoop-azure/src/test/resources/readVectored.plantuml +++ /dev/null @@ -1,28 +0,0 @@ -@startuml -'https://plantuml.com/sequence-diagram - -autonumber -Developer -> AbfsInputStream: readVectored(List ranges, IntFunction allocate) -AbfsInputStream -> VectoredReadUtils: getFinalRanges(ranges) -VectoredReadUtils -> AbfsInputStream : List getFinalRanges -loop range in finalRanges: -AbfsInputStream -> VectoredReadThreadUtils : getThread() -VectoredReadThreadUtils -> AbfsInputStream : thread -AbfsInputStream -> thread : invoke(range) -thread -> AbfsInputStream : control back to main thread -par threadExec: -loop buffer:multipleBuffersInRange -thread -> BufferManager : checkIfAlreadyQueued() -alt bufferIsNotQueued: -thread -> AbfsInputStream : read(bufferRange) -AbfsInputStream -> AbfsClient : execute() -AbfsClient -> AbfsInputStream : result -end -end -end -end -AbfsInputStream -> VectoredReadUtils : convertToDevUnderstandableData() -VectoredReadUtils -> AbfsInputStream : List> result -AbfsInputStream -> Developer : result - -@enduml \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/test/resources/readVectored.png b/hadoop-tools/hadoop-azure/src/test/resources/readVectored.png deleted file mode 100644 index e121eea052b310740b5574a1b70661a53e6fbd29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 211653 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 z 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 CtXY5n From db9ec05ae4e5a1e8434d09f0ce83746ceca35eda Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Thu, 19 Sep 2024 07:43:49 -0700 Subject: [PATCH 03/32] ABFS Backoff metrics using IOStatistics --- .../fs/azurebfs/AbfsBackoffMetrics.java | 278 ++++-------------- .../AbfsBackoffMetricsUsingIOStatistics.java | 60 ---- .../hadoop/fs/azurebfs/AbfsCountersImpl.java | 12 +- .../azurebfs/constants/MetricsConstants.java | 4 + .../enums/AbfsBackoffMetricsEnum.java | 13 +- .../fs/azurebfs/services/AbfsClient.java | 6 +- .../fs/azurebfs/services/AbfsCounters.java | 3 - .../azurebfs/services/AbfsRestOperation.java | 43 ++- .../AbstractAbfsStatisticsSource.java | 6 +- .../services/TestAbfsRestOperation.java | 3 +- 10 files changed, 107 insertions(+), 321 deletions(-) delete mode 100644 hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsBackoffMetricsUsingIOStatistics.java diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsBackoffMetrics.java index 37dbdfffeed6f..2e548bc51757e 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsBackoffMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsBackoffMetrics.java @@ -18,227 +18,71 @@ package org.apache.hadoop.fs.azurebfs; -import java.util.ArrayList; +import org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum; +import org.apache.hadoop.fs.azurebfs.statistics.AbstractAbfsStatisticsSource; +import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; + import java.util.Arrays; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Stream; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.HUNDRED; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.THOUSAND; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.RETRY; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.RETRY_LIST; +import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.iostatisticsStore; -public class AbfsBackoffMetrics { - - private AtomicLong numberOfRequestsSucceeded; - - private AtomicLong minBackoff; - - private AtomicLong maxBackoff; - - private AtomicLong totalRequests; - - private AtomicLong totalBackoff; - - private String retryCount; - - private AtomicLong numberOfIOPSThrottledRequests; - - private AtomicLong numberOfBandwidthThrottledRequests; - - private AtomicLong numberOfOtherThrottledRequests; - - private AtomicLong numberOfNetworkFailedRequests; - - private AtomicLong maxRetryCount; - - private AtomicLong totalNumberOfRequests; - - private AtomicLong numberOfRequestsSucceededWithoutRetrying; - - private AtomicLong numberOfRequestsFailed; - - private final Map metricsMap - = new ConcurrentHashMap<>(); +public class AbfsBackoffMetrics extends AbstractAbfsStatisticsSource { public AbfsBackoffMetrics() { - initializeMap(); - this.numberOfIOPSThrottledRequests = new AtomicLong(); - this.numberOfBandwidthThrottledRequests = new AtomicLong(); - this.numberOfOtherThrottledRequests = new AtomicLong(); - this.totalNumberOfRequests = new AtomicLong(); - this.maxRetryCount = new AtomicLong(); - this.numberOfRequestsSucceededWithoutRetrying = new AtomicLong(); - this.numberOfRequestsFailed = new AtomicLong(); - this.numberOfNetworkFailedRequests = new AtomicLong(); - } - - public AbfsBackoffMetrics(String retryCount) { - this.retryCount = retryCount; - this.numberOfRequestsSucceeded = new AtomicLong(); - this.minBackoff = new AtomicLong(Long.MAX_VALUE); - this.maxBackoff = new AtomicLong(); - this.totalRequests = new AtomicLong(); - this.totalBackoff = new AtomicLong(); - } - - private void initializeMap() { - ArrayList retryCountList = new ArrayList( - Arrays.asList("1", "2", "3", "4", "5_15", "15_25", "25AndAbove")); - for (String s : retryCountList) { - metricsMap.put(s, new AbfsBackoffMetrics(s)); - } - } - - public long getNumberOfRequestsSucceeded() { - return this.numberOfRequestsSucceeded.get(); - } - - public void setNumberOfRequestsSucceeded(long numberOfRequestsSucceeded) { - this.numberOfRequestsSucceeded.set(numberOfRequestsSucceeded); - } - - public void incrementNumberOfRequestsSucceeded() { - this.numberOfRequestsSucceeded.getAndIncrement(); - } - - public long getMinBackoff() { - return this.minBackoff.get(); - } - - public void setMinBackoff(long minBackoff) { - this.minBackoff.set(minBackoff); - } - - public long getMaxBackoff() { - return this.maxBackoff.get(); - } - - public void setMaxBackoff(long maxBackoff) { - this.maxBackoff.set(maxBackoff); - } - - public long getTotalRequests() { - return this.totalRequests.get(); - } - - public void incrementTotalRequests() { - this.totalRequests.incrementAndGet(); - } - - public void setTotalRequests(long totalRequests) { - this.totalRequests.set(totalRequests); - } - - public long getTotalBackoff() { - return this.totalBackoff.get(); - } - - public void setTotalBackoff(long totalBackoff) { - this.totalBackoff.set(totalBackoff); - } - - public String getRetryCount() { - return this.retryCount; - } - - public long getNumberOfIOPSThrottledRequests() { - return this.numberOfIOPSThrottledRequests.get(); - } - - public void setNumberOfIOPSThrottledRequests(long numberOfIOPSThrottledRequests) { - this.numberOfIOPSThrottledRequests.set(numberOfIOPSThrottledRequests); - } - - public void incrementNumberOfIOPSThrottledRequests() { - this.numberOfIOPSThrottledRequests.getAndIncrement(); - } - - public long getNumberOfBandwidthThrottledRequests() { - return this.numberOfBandwidthThrottledRequests.get(); - } - - public void setNumberOfBandwidthThrottledRequests(long numberOfBandwidthThrottledRequests) { - this.numberOfBandwidthThrottledRequests.set(numberOfBandwidthThrottledRequests); - } - - public void incrementNumberOfBandwidthThrottledRequests() { - this.numberOfBandwidthThrottledRequests.getAndIncrement(); - } - - public long getNumberOfOtherThrottledRequests() { - return this.numberOfOtherThrottledRequests.get(); - } - - public void setNumberOfOtherThrottledRequests(long numberOfOtherThrottledRequests) { - this.numberOfOtherThrottledRequests.set(numberOfOtherThrottledRequests); - } - - public void incrementNumberOfOtherThrottledRequests() { - this.numberOfOtherThrottledRequests.getAndIncrement(); - } - - public long getMaxRetryCount() { - return this.maxRetryCount.get(); - } - - public void setMaxRetryCount(long maxRetryCount) { - this.maxRetryCount.set(maxRetryCount); - } - - public void incrementMaxRetryCount() { - this.maxRetryCount.getAndIncrement(); - } - - public long getTotalNumberOfRequests() { - return this.totalNumberOfRequests.get(); - } - - public void setTotalNumberOfRequests(long totalNumberOfRequests) { - this.totalNumberOfRequests.set(totalNumberOfRequests); - } - - public void incrementTotalNumberOfRequests() { - this.totalNumberOfRequests.getAndIncrement(); - } - - public Map getMetricsMap() { - return metricsMap; + IOStatisticsStore ioStatisticsStore = iostatisticsStore() + .withCounters(getCountersName()) + .build(); + setIOStatistics(ioStatisticsStore); } - public long getNumberOfRequestsSucceededWithoutRetrying() { - return this.numberOfRequestsSucceededWithoutRetrying.get(); + private String[] getCountersName() { + return Arrays.stream(AbfsBackoffMetricsEnum.values()) + .flatMap(backoffMetricsEnum -> { + if (RETRY.equals(backoffMetricsEnum.getType())) { + return RETRY_LIST.stream() + .map(retryCount -> retryCount + ":" + backoffMetricsEnum.getName()); + } else { + return Stream.of(backoffMetricsEnum.getName()); + } + }) + .toArray(String[]::new); } - public void setNumberOfRequestsSucceededWithoutRetrying(long numberOfRequestsSucceededWithoutRetrying) { - this.numberOfRequestsSucceededWithoutRetrying.set(numberOfRequestsSucceededWithoutRetrying); + public void incrementCounter(AbfsBackoffMetricsEnum metric, String retryCount, long value) { + incCounter(retryCount + ":" + metric.getName(), value); } - public void incrementNumberOfRequestsSucceededWithoutRetrying() { - this.numberOfRequestsSucceededWithoutRetrying.getAndIncrement(); + public void incrementCounter(AbfsBackoffMetricsEnum metric, String retryCount) { + incrementCounter(metric, retryCount, 1); } - public long getNumberOfRequestsFailed() { - return this.numberOfRequestsFailed.get(); + public void incrementCounter(AbfsBackoffMetricsEnum metric, long value) { + incCounter(metric.getName(), value); } - public void setNumberOfRequestsFailed(long numberOfRequestsFailed) { - this.numberOfRequestsFailed.set(numberOfRequestsFailed); + public void incrementCounter(AbfsBackoffMetricsEnum metric) { + incrementCounter(metric, 1); } - public void incrementNumberOfRequestsFailed() { - this.numberOfRequestsFailed.getAndIncrement(); + public Long getCounter(AbfsBackoffMetricsEnum metric, String retryCount) { + return lookupCounterValue(retryCount + ":" + metric.getName()); } - public long getNumberOfNetworkFailedRequests() { - return this.numberOfNetworkFailedRequests.get(); + public Long getCounter(AbfsBackoffMetricsEnum metric) { + return lookupCounterValue(metric.getName()); } - public void setNumberOfNetworkFailedRequests(long numberOfNetworkFailedRequests) { - this.numberOfNetworkFailedRequests.set(numberOfNetworkFailedRequests); + public void setCounter(AbfsBackoffMetricsEnum metric, String retryCount, long value) { + setCounterValue(retryCount + ":" + metric.getName(), value); } - public void incrementNumberOfNetworkFailedRequests() { - this.numberOfNetworkFailedRequests.getAndIncrement(); + public void setCounter(AbfsBackoffMetricsEnum metric, long value) { + setCounterValue(metric.getName(), 1); } /* @@ -259,52 +103,52 @@ public void incrementNumberOfNetworkFailedRequests() { @Override public String toString() { StringBuilder metricString = new StringBuilder(); - long totalRequestsThrottled = getNumberOfBandwidthThrottledRequests() - + getNumberOfIOPSThrottledRequests() - + getNumberOfOtherThrottledRequests(); + long totalRequestsThrottled = getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS) + + getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_IOPS_THROTTLED_REQUESTS) + + getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_OTHER_THROTTLED_REQUESTS); double percentageOfRequestsThrottled = - ((double) totalRequestsThrottled / getTotalNumberOfRequests()) * HUNDRED; - for (Map.Entry entry : metricsMap.entrySet()) { - metricString.append("$RCTSI$_").append(entry.getKey()) + ((double) totalRequestsThrottled / getCounter(AbfsBackoffMetricsEnum.TOTAL_NUMBER_OF_REQUESTS)) * HUNDRED; + for (String retryCount: RETRY_LIST) { + metricString.append("$RCTSI$_").append(retryCount) .append("R_").append("=") - .append(entry.getValue().getNumberOfRequestsSucceeded()); - long totalRequests = entry.getValue().getTotalRequests(); + .append(getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED, retryCount)); + long totalRequests = getCounter(AbfsBackoffMetricsEnum.TOTAL_REQUESTS, retryCount); if (totalRequests > 0) { - metricString.append("$MMA$_").append(entry.getKey()) + metricString.append("$MMA$_").append(retryCount) .append("R_").append("=") .append(String.format("%.3f", - (double) entry.getValue().getMinBackoff() / THOUSAND)) + (double) getCounter(AbfsBackoffMetricsEnum.MIN_BACK_OFF, retryCount) / THOUSAND)) .append("s") .append(String.format("%.3f", - (double) entry.getValue().getMaxBackoff() / THOUSAND)) + (double) getCounter(AbfsBackoffMetricsEnum.MAX_BACK_OFF, retryCount) / THOUSAND)) .append("s") .append(String.format("%.3f", - ((double) entry.getValue().getTotalBackoff() / totalRequests) + ((double) getCounter(AbfsBackoffMetricsEnum.TOTAL_BACK_OFF, retryCount) / totalRequests) / THOUSAND)) .append("s"); } else { - metricString.append("$MMA$_").append(entry.getKey()) + metricString.append("$MMA$_").append(retryCount) .append("R_").append("=0s"); } } metricString.append("$BWT=") - .append(getNumberOfBandwidthThrottledRequests()) + .append(getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS)) .append("$IT=") - .append(getNumberOfIOPSThrottledRequests()) + .append(getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_IOPS_THROTTLED_REQUESTS)) .append("$OT=") - .append(getNumberOfOtherThrottledRequests()) + .append(getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_OTHER_THROTTLED_REQUESTS)) .append("$RT=") .append(String.format("%.3f", percentageOfRequestsThrottled)) .append("$NFR=") - .append(getNumberOfNetworkFailedRequests()) + .append(getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_NETWORK_FAILED_REQUESTS)) .append("$TRNR=") - .append(getNumberOfRequestsSucceededWithoutRetrying()) + .append(getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING)) .append("$TRF=") - .append(getNumberOfRequestsFailed()) + .append(getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_FAILED)) .append("$TR=") - .append(getTotalNumberOfRequests()) + .append(getCounter(AbfsBackoffMetricsEnum.TOTAL_NUMBER_OF_REQUESTS)) .append("$MRC=") - .append(getMaxRetryCount()); + .append(getCounter(AbfsBackoffMetricsEnum.MAX_RETRY_COUNT)); return metricString + ""; } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsBackoffMetricsUsingIOStatistics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsBackoffMetricsUsingIOStatistics.java deleted file mode 100644 index be3df897ab4d1..0000000000000 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsBackoffMetricsUsingIOStatistics.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.apache.hadoop.fs.azurebfs; - -import org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum; -import org.apache.hadoop.fs.azurebfs.statistics.AbstractAbfsStatisticsSource; -import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; - -import java.util.Arrays; -import java.util.List; -import java.util.stream.Stream; - -import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.RETRY; -import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.iostatisticsStore; - -public class AbfsBackoffMetricsUsingIOStatistics extends AbstractAbfsStatisticsSource { - private final List retryCountList = Arrays.asList("1", "2", "3", "4", "5_15", "15_25", "25AndAbove"); - - public AbfsBackoffMetricsUsingIOStatistics() { - IOStatisticsStore ioStatisticsStore = iostatisticsStore() - .withCounters(getCountersName()) - .build(); - setIOStatistics(ioStatisticsStore); - } - - private String[] getCountersName() { - return Arrays.stream(AbfsBackoffMetricsEnum.values()) - .flatMap(backoffMetricsEnum -> { - if (RETRY.equals(backoffMetricsEnum.getType())) { - return retryCountList.stream() - .map(retryCount -> retryCount + ":" + backoffMetricsEnum.getName()); - } else { - return Stream.of(backoffMetricsEnum.getName()); - } - }) - .toArray(String[]::new); - } - - public void incrementCounter(AbfsBackoffMetricsEnum metric, String retryCount, long value) { - incCounter(retryCount + ":" + metric.getName(), value); - } - - public void incrementCounter(AbfsBackoffMetricsEnum metric, String retryCount) { - incrementCounter(metric, retryCount, 1); - } - - public void incrementCounter(AbfsBackoffMetricsEnum metric, long value) { - incCounter(metric.getName(), value); - } - - public void incrementCounter(AbfsBackoffMetricsEnum metric) { - incrementCounter(metric, 1); - } - - public Long lookupCounterValue(AbfsBackoffMetricsEnum metric, String retryCount) { - return lookupCounterValue(retryCount + ":" + metric.getName()); - } - - public Long lookupCounterValue(AbfsBackoffMetricsEnum metric) { - return lookupCounterValue(metric.getName()); - } -} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java index c56b9918ee7c4..f3a1f8c395764 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java @@ -102,8 +102,6 @@ public class AbfsCountersImpl implements AbfsCounters { private AbfsBackoffMetrics abfsBackoffMetrics = null; - private AbfsBackoffMetricsUsingIOStatistics abfsBackoffMetricsUsingIOStatistics = null; - private AbfsReadFooterMetrics abfsReadFooterMetrics = null; private AtomicLong lastExecutionTime = null; @@ -173,9 +171,6 @@ public void initializeMetrics(MetricFormat metricFormat) { switch (metricFormat) { case INTERNAL_BACKOFF_METRIC_FORMAT: abfsBackoffMetrics = new AbfsBackoffMetrics(); - abfsBackoffMetricsUsingIOStatistics = new AbfsBackoffMetricsUsingIOStatistics(); - abfsBackoffMetricsUsingIOStatistics.incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED, "1"); - System.out.println(abfsBackoffMetricsUsingIOStatistics.toString()); break; case INTERNAL_FOOTER_METRIC_FORMAT: abfsReadFooterMetrics = new AbfsReadFooterMetrics(); @@ -259,11 +254,6 @@ public AbfsBackoffMetrics getAbfsBackoffMetrics() { return abfsBackoffMetrics != null ? abfsBackoffMetrics : null; } - @Override - public AbfsBackoffMetricsUsingIOStatistics getAbfsBackoffMetricsUsingIOStatistics() { - return abfsBackoffMetricsUsingIOStatistics; - } - @Override public AtomicLong getLastExecutionTime() { return lastExecutionTime; @@ -335,7 +325,7 @@ public DurationTracker trackDuration(String key) { public String toString() { String metric = ""; if (abfsBackoffMetrics != null) { - long totalNoRequests = getAbfsBackoffMetrics().getTotalNumberOfRequests(); + long totalNoRequests = getAbfsBackoffMetrics().getCounter(AbfsBackoffMetricsEnum.TOTAL_NUMBER_OF_REQUESTS); if (totalNoRequests > 0) { metric += "#BO:" + getAbfsBackoffMetrics().toString(); } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java index be0e2981a39f4..5ab58628c0d9c 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java @@ -1,6 +1,10 @@ package org.apache.hadoop.fs.azurebfs.constants; +import java.util.Arrays; +import java.util.List; + public final class MetricsConstants { public static final String RETRY = "RETRY"; public static final String BASE = "BASE"; + public static final List RETRY_LIST = Arrays.asList("1", "2", "3", "4", "5_15", "15_25", "25AndAbove"); } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java index c24ec273efff5..407009411fd98 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java @@ -4,8 +4,19 @@ import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.RETRY; public enum AbfsBackoffMetricsEnum { + NUMBER_OF_IOPS_THROTTLED_REQUESTS("numberOfIOPSThrottledRequests", BASE, "Number of IOPS throttled requests", "abc"), + NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS("numberOfBandwidthThrottledRequests", BASE, "Number of bandwidth throttled requests", "def"), + NUMBER_OF_OTHER_THROTTLED_REQUESTS("numberOfOtherThrottledRequests", BASE, "Number of other throttled requests", "def"), + NUMBER_OF_NETWORK_FAILED_REQUESTS("numberOfNetworkFailedRequests", BASE, "Number of network failed requests", "def"), + MAX_RETRY_COUNT("maxRetryCount", BASE, "Max retry count", "def"), + TOTAL_NUMBER_OF_REQUESTS("totalNumberOfRequests", BASE, "Total number of requests", "def"), + NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING("numberOfRequestsSucceededWithoutRetrying", BASE, "Number of requests succeeded without retrying", "def"), + NUMBER_OF_REQUESTS_FAILED("numberOfRequestsFailed", BASE, "Number of requests failed", "def"), NUMBER_OF_REQUESTS_SUCCEEDED("numberOfRequestsSucceeded", RETRY,"Number of requests succeeded", "xyz"), - NUMBER_OF_IOPS_THROTTLED_REQUESTS("numberOfIOPSThrottledRequests", BASE, "Number of IOPS throttled requests", "abc"),; + MIN_BACK_OFF("minBackOff", RETRY, "Min back off", "def"), + MAX_BACK_OFF("maxBackOff", RETRY, "Max back off", "def"), + TOTAL_REQUESTS("totalRequests", RETRY, "Total requests", "def"), + TOTAL_BACK_OFF("totalBackoff", RETRY, "Total back off", "def"); private final String name; private final String type; diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClient.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClient.java index a2d65c145b625..8dc733dfdefe2 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClient.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClient.java @@ -122,7 +122,7 @@ public class AbfsClient implements Closeable { private AccessTokenProvider tokenProvider; private SASTokenProvider sasTokenProvider; private final AbfsCounters abfsCounters; - private final Timer timer; + private Timer timer = null; private final String abfsMetricUrl; private boolean isMetricCollectionEnabled = false; private final MetricFormat metricFormat; @@ -231,9 +231,9 @@ private AbfsClient(final URL baseUrl, throw new IOException("Exception while initializing metric credentials " + e); } } - this.timer = new Timer( - "abfs-timer-client", true); if (isMetricCollectionEnabled) { + this.timer = new Timer( + "abfs-timer-client", true); timer.schedule(new TimerTaskImpl(), metricIdlePeriod, metricIdlePeriod); diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsCounters.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsCounters.java index f490013b0668c..65e5fa29a138b 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsCounters.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsCounters.java @@ -26,7 +26,6 @@ import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; import org.apache.hadoop.fs.azurebfs.AbfsBackoffMetrics; -import org.apache.hadoop.fs.azurebfs.AbfsBackoffMetricsUsingIOStatistics; import org.apache.hadoop.fs.azurebfs.AbfsStatistic; import org.apache.hadoop.fs.azurebfs.utils.MetricFormat; import org.apache.hadoop.fs.statistics.DurationTracker; @@ -83,8 +82,6 @@ String formString(String prefix, String separator, String suffix, AbfsBackoffMetrics getAbfsBackoffMetrics(); - AbfsBackoffMetricsUsingIOStatistics getAbfsBackoffMetricsUsingIOStatistics(); - AbfsReadFooterMetrics getAbfsReadFooterMetrics(); AtomicLong getLastExecutionTime(); diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java index 1cdc9e20c0f77..7195b6e9c993e 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java @@ -26,6 +26,7 @@ import java.time.Duration; import java.util.List; +import org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -93,7 +94,6 @@ public class AbfsRestOperation { private AbfsHttpOperation result; private final AbfsCounters abfsCounters; private AbfsBackoffMetrics abfsBackoffMetrics; - private Map metricsMap; /** * This variable contains the reason of last API call within the same * AbfsRestOperation object. @@ -198,9 +198,6 @@ String getSasToken() { if (abfsCounters != null) { this.abfsBackoffMetrics = abfsCounters.getAbfsBackoffMetrics(); } - if (abfsBackoffMetrics != null) { - this.metricsMap = abfsBackoffMetrics.getMetricsMap(); - } this.maxIoRetries = abfsConfiguration.getMaxIoRetries(); this.intercept = client.getIntercept(); this.abfsConfiguration = abfsConfiguration; @@ -286,7 +283,7 @@ void completeExecute(TracingContext tracingContext) long sleepDuration = 0L; if (abfsBackoffMetrics != null) { synchronized (this) { - abfsBackoffMetrics.incrementTotalNumberOfRequests(); + abfsBackoffMetrics.incrementCounter(AbfsBackoffMetricsEnum.TOTAL_NUMBER_OF_REQUESTS); } } while (!executeHttpOperation(retryCount, tracingContext)) { @@ -332,17 +329,17 @@ void updateBackoffMetrics(int retryCount, int statusCode) { || statusCode >= HttpURLConnection.HTTP_INTERNAL_ERROR) { synchronized (this) { if (retryCount >= maxIoRetries) { - abfsBackoffMetrics.incrementNumberOfRequestsFailed(); + abfsBackoffMetrics.incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_FAILED); } } } else { synchronized (this) { if (retryCount > ZERO && retryCount <= maxIoRetries) { - maxRetryCount = Math.max(abfsBackoffMetrics.getMaxRetryCount(), retryCount); - abfsBackoffMetrics.setMaxRetryCount(maxRetryCount); + maxRetryCount = Math.max(abfsBackoffMetrics.getCounter(AbfsBackoffMetricsEnum.MAX_RETRY_COUNT), retryCount); + abfsBackoffMetrics.setCounter(AbfsBackoffMetricsEnum.MAX_RETRY_COUNT, maxRetryCount); updateCount(retryCount); } else { - abfsBackoffMetrics.incrementNumberOfRequestsSucceededWithoutRetrying(); + abfsBackoffMetrics.incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING); } } } @@ -408,12 +405,12 @@ private boolean executeHttpOperation(final int retryCount, AzureServiceErrorCode.INGRESS_OVER_ACCOUNT_LIMIT) || serviceErrorCode.equals( AzureServiceErrorCode.EGRESS_OVER_ACCOUNT_LIMIT)) { - abfsBackoffMetrics.incrementNumberOfBandwidthThrottledRequests(); + abfsBackoffMetrics.incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS); } else if (serviceErrorCode.equals( AzureServiceErrorCode.TPS_OVER_ACCOUNT_LIMIT)) { - abfsBackoffMetrics.incrementNumberOfIOPSThrottledRequests(); + abfsBackoffMetrics.incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_IOPS_THROTTLED_REQUESTS); } else { - abfsBackoffMetrics.incrementNumberOfOtherThrottledRequests(); + abfsBackoffMetrics.incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_OTHER_THROTTLED_REQUESTS); } } } @@ -459,7 +456,7 @@ private boolean executeHttpOperation(final int retryCount, } if (abfsBackoffMetrics != null) { synchronized (this) { - abfsBackoffMetrics.incrementNumberOfNetworkFailedRequests(); + abfsBackoffMetrics.incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_NETWORK_FAILED_REQUESTS); } } if (!retryPolicy.shouldRetry(retryCount, -1)) { @@ -474,7 +471,7 @@ private boolean executeHttpOperation(final int retryCount, } if (abfsBackoffMetrics != null) { synchronized (this) { - abfsBackoffMetrics.incrementNumberOfNetworkFailedRequests(); + abfsBackoffMetrics.incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_NETWORK_FAILED_REQUESTS); } } failureReason = RetryReason.getAbbreviation(ex, -1, ""); @@ -604,7 +601,7 @@ private void incrementCounter(AbfsStatistic statistic, long value) { */ private void updateCount(int retryCount){ String retryCounter = getKey(retryCount); - metricsMap.get(retryCounter).incrementNumberOfRequestsSucceeded(); + abfsBackoffMetrics.incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED, retryCounter); } /** @@ -618,15 +615,13 @@ private void updateCount(int retryCount){ private void updateBackoffTimeMetrics(int retryCount, long sleepDuration) { synchronized (this) { String retryCounter = getKey(retryCount); - AbfsBackoffMetrics abfsBackoffMetrics = metricsMap.get(retryCounter); - long minBackoffTime = Math.min(abfsBackoffMetrics.getMinBackoff(), sleepDuration); - long maxBackoffForTime = Math.max(abfsBackoffMetrics.getMaxBackoff(), sleepDuration); - long totalBackoffTime = abfsBackoffMetrics.getTotalBackoff() + sleepDuration; - abfsBackoffMetrics.incrementTotalRequests(); - abfsBackoffMetrics.setMinBackoff(minBackoffTime); - abfsBackoffMetrics.setMaxBackoff(maxBackoffForTime); - abfsBackoffMetrics.setTotalBackoff(totalBackoffTime); - metricsMap.put(retryCounter, abfsBackoffMetrics); + long minBackoffTime = Math.min(abfsBackoffMetrics.getCounter(AbfsBackoffMetricsEnum.MIN_BACK_OFF, retryCounter), sleepDuration); + long maxBackoffForTime = Math.max(abfsBackoffMetrics.getCounter(AbfsBackoffMetricsEnum.MAX_BACK_OFF, retryCounter), sleepDuration); + long totalBackoffTime = abfsBackoffMetrics.getCounter(AbfsBackoffMetricsEnum.TOTAL_BACK_OFF, retryCounter) + sleepDuration; + abfsBackoffMetrics.incrementCounter(AbfsBackoffMetricsEnum.TOTAL_REQUESTS, retryCounter); + abfsBackoffMetrics.setCounter(AbfsBackoffMetricsEnum.MIN_BACK_OFF, retryCounter, minBackoffTime); + abfsBackoffMetrics.setCounter(AbfsBackoffMetricsEnum.MAX_BACK_OFF, retryCounter, maxBackoffForTime); + abfsBackoffMetrics.setCounter(AbfsBackoffMetricsEnum.TOTAL_BACK_OFF, retryCounter, totalBackoffTime); } } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java index 69ed09ace8a5d..6512cf647503a 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java @@ -26,10 +26,14 @@ public long incCounter(String name, long value) { return ioStatistics.incrementCounter(name, value); } - public Long lookupCounterValue(final String name) { + public Long lookupCounterValue(String name) { return ioStatistics.counters().get(name); } + public void setCounterValue(String name, long value) { + ioStatistics.setCounter(name, value); + } + @Override public String toString() { final StringBuilder sb = new StringBuilder( diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsRestOperation.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsRestOperation.java index e99d05d41ca60..dee9b7a6a00fc 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsRestOperation.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsRestOperation.java @@ -21,6 +21,7 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.azurebfs.AbfsConfiguration; +import org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum; import org.apache.hadoop.fs.azurebfs.utils.MetricFormat; import org.junit.Test; import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.HTTP_METHOD_DELETE; @@ -73,7 +74,7 @@ public void testBackoffRetryMetrics() throws Exception { // For retry count greater than the max configured value, the request should fail. Assert.assertEquals("Number of failed requests does not match expected value.", - "3", String.valueOf(testClient.getAbfsCounters().getAbfsBackoffMetrics().getNumberOfRequestsFailed())); + "3", String.valueOf(testClient.getAbfsCounters().getAbfsBackoffMetrics().getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_FAILED))); // Close the AzureBlobFileSystem. fs.close(); From 6531e8360162a4b39c007d2ca24bd8c1bd6b5db6 Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Tue, 24 Sep 2024 05:56:01 -0700 Subject: [PATCH 04/32] AbfsBackoff metrics changes --- .../fs/azurebfs/AbfsBackoffMetrics.java | 140 +++++++++++++++--- .../hadoop/fs/azurebfs/AbfsCountersImpl.java | 3 +- .../enums/AbfsBackoffMetricsEnum.java | 34 ++--- .../azurebfs/services/AbfsRestOperation.java | 37 +++-- .../services/TestAbfsRestOperation.java | 3 +- 5 files changed, 156 insertions(+), 61 deletions(-) diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsBackoffMetrics.java index 2e548bc51757e..2cec974b47e2c 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsBackoffMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsBackoffMetrics.java @@ -82,7 +82,111 @@ public void setCounter(AbfsBackoffMetricsEnum metric, String retryCount, long va } public void setCounter(AbfsBackoffMetricsEnum metric, long value) { - setCounterValue(metric.getName(), 1); + setCounterValue(metric.getName(), value); + } + + public long getNumberOfIOPSThrottledRequests() { + return getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_IOPS_THROTTLED_REQUESTS); + } + + public void incrementNumberOfIOPSThrottledRequests() { + incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_IOPS_THROTTLED_REQUESTS); + } + + public long getNumberOfBandwidthThrottledRequests() { + return getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS); + } + + public void incrementNumberOfBandwidthThrottledRequests() { + incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS); + } + + public long getNumberOfOtherThrottledRequests() { + return getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_OTHER_THROTTLED_REQUESTS); + } + + public void incrementNumberOfOtherThrottledRequests() { + incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_OTHER_THROTTLED_REQUESTS); + } + + public long getNumberOfNetworkFailedRequests() { + return getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_NETWORK_FAILED_REQUESTS); + } + + public void incrementNumberOfNetworkFailedRequests() { + incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_NETWORK_FAILED_REQUESTS); + } + + public long getMaxRetryCount() { + return getCounter(AbfsBackoffMetricsEnum.MAX_RETRY_COUNT); + } + + public void setMaxRetryCount(long value) { + setCounter(AbfsBackoffMetricsEnum.MAX_RETRY_COUNT, value); + } + + public long getTotalNumberOfRequests() { + return getCounter(AbfsBackoffMetricsEnum.TOTAL_NUMBER_OF_REQUESTS); + } + + public void incrementTotalNumberOfRequests() { + incrementCounter(AbfsBackoffMetricsEnum.TOTAL_NUMBER_OF_REQUESTS); + } + + public long getNumberOfRequestsSucceededWithoutRetrying() { + return getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING); + } + + public void incrementNumberOfRequestsSucceededWithoutRetrying() { + incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING); + } + + public long getNumberOfRequestsFailed() { + return getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_FAILED); + } + + public void incrementNumberOfRequestsFailed() { + incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_FAILED); + } + + public long getNumberOfRequestsSucceeded(String retryCount) { + return getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED, retryCount); + } + + public void incrementNumberOfRequestsSucceeded(String retryCount) { + incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED, retryCount); + } + + public long getMinBackoff(String retryCount) { + return getCounter(AbfsBackoffMetricsEnum.MIN_BACK_OFF, retryCount); + } + + public void setMinBackoff(String retryCount, long value) { + setCounter(AbfsBackoffMetricsEnum.MIN_BACK_OFF, retryCount, value); + } + + public long getMaxBackoff(String retryCount) { + return getCounter(AbfsBackoffMetricsEnum.MAX_BACK_OFF, retryCount); + } + + public void setMaxBackoff(String retryCount, long value) { + setCounter(AbfsBackoffMetricsEnum.MAX_BACK_OFF, retryCount, value); + } + + public long getTotalBackoff(String retryCount) { + return getCounter(AbfsBackoffMetricsEnum.TOTAL_BACK_OFF, retryCount); + } + + public void setTotalBackoff(String retryCount, long value) { + setCounter(AbfsBackoffMetricsEnum.TOTAL_BACK_OFF, retryCount, value); + } + + public long getTotalRequests(String retryCount) { + return getCounter(AbfsBackoffMetricsEnum.TOTAL_REQUESTS, retryCount); + } + + public void incrementTotalRequests(String retryCount) { + incrementCounter(AbfsBackoffMetricsEnum.TOTAL_REQUESTS, retryCount); } /* @@ -103,27 +207,27 @@ public void setCounter(AbfsBackoffMetricsEnum metric, long value) { @Override public String toString() { StringBuilder metricString = new StringBuilder(); - long totalRequestsThrottled = getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS) - + getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_IOPS_THROTTLED_REQUESTS) - + getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_OTHER_THROTTLED_REQUESTS); + long totalRequestsThrottled = getNumberOfBandwidthThrottledRequests() + + getNumberOfIOPSThrottledRequests() + + getNumberOfOtherThrottledRequests(); double percentageOfRequestsThrottled = - ((double) totalRequestsThrottled / getCounter(AbfsBackoffMetricsEnum.TOTAL_NUMBER_OF_REQUESTS)) * HUNDRED; + ((double) totalRequestsThrottled / getTotalNumberOfRequests()) * HUNDRED; for (String retryCount: RETRY_LIST) { metricString.append("$RCTSI$_").append(retryCount) .append("R_").append("=") - .append(getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED, retryCount)); - long totalRequests = getCounter(AbfsBackoffMetricsEnum.TOTAL_REQUESTS, retryCount); + .append(getNumberOfRequestsSucceeded(retryCount)); + long totalRequests = getTotalRequests(retryCount); if (totalRequests > 0) { metricString.append("$MMA$_").append(retryCount) .append("R_").append("=") .append(String.format("%.3f", - (double) getCounter(AbfsBackoffMetricsEnum.MIN_BACK_OFF, retryCount) / THOUSAND)) + (double) getMinBackoff(retryCount) / THOUSAND)) .append("s") .append(String.format("%.3f", - (double) getCounter(AbfsBackoffMetricsEnum.MAX_BACK_OFF, retryCount) / THOUSAND)) + (double) getMaxBackoff(retryCount) / THOUSAND)) .append("s") .append(String.format("%.3f", - ((double) getCounter(AbfsBackoffMetricsEnum.TOTAL_BACK_OFF, retryCount) / totalRequests) + ((double) getTotalBackoff(retryCount) / totalRequests) / THOUSAND)) .append("s"); } else { @@ -132,23 +236,23 @@ public String toString() { } } metricString.append("$BWT=") - .append(getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS)) + .append(getNumberOfBandwidthThrottledRequests()) .append("$IT=") - .append(getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_IOPS_THROTTLED_REQUESTS)) + .append(getNumberOfIOPSThrottledRequests()) .append("$OT=") - .append(getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_OTHER_THROTTLED_REQUESTS)) + .append(getNumberOfOtherThrottledRequests()) .append("$RT=") .append(String.format("%.3f", percentageOfRequestsThrottled)) .append("$NFR=") - .append(getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_NETWORK_FAILED_REQUESTS)) + .append(getNumberOfNetworkFailedRequests()) .append("$TRNR=") - .append(getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING)) + .append(getNumberOfRequestsSucceededWithoutRetrying()) .append("$TRF=") - .append(getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_FAILED)) + .append(getNumberOfRequestsFailed()) .append("$TR=") - .append(getCounter(AbfsBackoffMetricsEnum.TOTAL_NUMBER_OF_REQUESTS)) + .append(getTotalNumberOfRequests()) .append("$MRC=") - .append(getCounter(AbfsBackoffMetricsEnum.MAX_RETRY_COUNT)); + .append(getMaxRetryCount()); return metricString + ""; } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java index f3a1f8c395764..c4d3e05cdb25d 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java @@ -24,7 +24,6 @@ import java.util.concurrent.atomic.AtomicLong; import org.apache.hadoop.classification.VisibleForTesting; -import org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum; import org.apache.hadoop.fs.azurebfs.services.AbfsCounters; import org.apache.hadoop.fs.azurebfs.services.AbfsReadFooterMetrics; import org.apache.hadoop.fs.azurebfs.utils.MetricFormat; @@ -325,7 +324,7 @@ public DurationTracker trackDuration(String key) { public String toString() { String metric = ""; if (abfsBackoffMetrics != null) { - long totalNoRequests = getAbfsBackoffMetrics().getCounter(AbfsBackoffMetricsEnum.TOTAL_NUMBER_OF_REQUESTS); + long totalNoRequests = getAbfsBackoffMetrics().getTotalNumberOfRequests(); if (totalNoRequests > 0) { metric += "#BO:" + getAbfsBackoffMetrics().toString(); } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java index 407009411fd98..85f1fe1be3e0e 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java @@ -4,30 +4,28 @@ import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.RETRY; public enum AbfsBackoffMetricsEnum { - NUMBER_OF_IOPS_THROTTLED_REQUESTS("numberOfIOPSThrottledRequests", BASE, "Number of IOPS throttled requests", "abc"), - NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS("numberOfBandwidthThrottledRequests", BASE, "Number of bandwidth throttled requests", "def"), - NUMBER_OF_OTHER_THROTTLED_REQUESTS("numberOfOtherThrottledRequests", BASE, "Number of other throttled requests", "def"), - NUMBER_OF_NETWORK_FAILED_REQUESTS("numberOfNetworkFailedRequests", BASE, "Number of network failed requests", "def"), - MAX_RETRY_COUNT("maxRetryCount", BASE, "Max retry count", "def"), - TOTAL_NUMBER_OF_REQUESTS("totalNumberOfRequests", BASE, "Total number of requests", "def"), - NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING("numberOfRequestsSucceededWithoutRetrying", BASE, "Number of requests succeeded without retrying", "def"), - NUMBER_OF_REQUESTS_FAILED("numberOfRequestsFailed", BASE, "Number of requests failed", "def"), - NUMBER_OF_REQUESTS_SUCCEEDED("numberOfRequestsSucceeded", RETRY,"Number of requests succeeded", "xyz"), - MIN_BACK_OFF("minBackOff", RETRY, "Min back off", "def"), - MAX_BACK_OFF("maxBackOff", RETRY, "Max back off", "def"), - TOTAL_REQUESTS("totalRequests", RETRY, "Total requests", "def"), - TOTAL_BACK_OFF("totalBackoff", RETRY, "Total back off", "def"); + NUMBER_OF_IOPS_THROTTLED_REQUESTS("numberOfIOPSThrottledRequests", BASE, "Number of IOPS throttled requests"), + NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS("numberOfBandwidthThrottledRequests", BASE, "Number of bandwidth throttled requests"), + NUMBER_OF_OTHER_THROTTLED_REQUESTS("numberOfOtherThrottledRequests", BASE, "Number of other throttled requests"), + NUMBER_OF_NETWORK_FAILED_REQUESTS("numberOfNetworkFailedRequests", BASE, "Number of network failed requests"), + MAX_RETRY_COUNT("maxRetryCount", BASE, "Max retry count"), + TOTAL_NUMBER_OF_REQUESTS("totalNumberOfRequests", BASE, "Total number of requests"), + NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING("numberOfRequestsSucceededWithoutRetrying", BASE, "Number of requests succeeded without retrying"), + NUMBER_OF_REQUESTS_FAILED("numberOfRequestsFailed", BASE, "Number of requests failed"), + NUMBER_OF_REQUESTS_SUCCEEDED("numberOfRequestsSucceeded", RETRY,"Number of requests succeeded"), + MIN_BACK_OFF("minBackOff", RETRY, "Minimum backoff"), + MAX_BACK_OFF("maxBackOff", RETRY, "Maximum backoff"), + TOTAL_BACK_OFF("totalBackoff", RETRY, "Total backoff"), + TOTAL_REQUESTS("totalRequests", RETRY, "Total requests"); private final String name; private final String type; private final String description; - private final String code; - AbfsBackoffMetricsEnum(String name, String type, String description, String code) { + AbfsBackoffMetricsEnum(String name, String type, String description) { this.name = name; this.type = type; this.description = description; - this.code = code; } public String getName() { @@ -41,8 +39,4 @@ public String getType() { public String getDescription() { return description; } - - public String getCode() { - return code; - } } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java index 7195b6e9c993e..5da92045406ea 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java @@ -26,7 +26,6 @@ import java.time.Duration; import java.util.List; -import org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -283,7 +282,7 @@ void completeExecute(TracingContext tracingContext) long sleepDuration = 0L; if (abfsBackoffMetrics != null) { synchronized (this) { - abfsBackoffMetrics.incrementCounter(AbfsBackoffMetricsEnum.TOTAL_NUMBER_OF_REQUESTS); + abfsBackoffMetrics.incrementTotalNumberOfRequests(); } } while (!executeHttpOperation(retryCount, tracingContext)) { @@ -329,17 +328,17 @@ void updateBackoffMetrics(int retryCount, int statusCode) { || statusCode >= HttpURLConnection.HTTP_INTERNAL_ERROR) { synchronized (this) { if (retryCount >= maxIoRetries) { - abfsBackoffMetrics.incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_FAILED); + abfsBackoffMetrics.incrementNumberOfRequestsFailed(); } } } else { synchronized (this) { if (retryCount > ZERO && retryCount <= maxIoRetries) { - maxRetryCount = Math.max(abfsBackoffMetrics.getCounter(AbfsBackoffMetricsEnum.MAX_RETRY_COUNT), retryCount); - abfsBackoffMetrics.setCounter(AbfsBackoffMetricsEnum.MAX_RETRY_COUNT, maxRetryCount); + maxRetryCount = Math.max(abfsBackoffMetrics.getMaxRetryCount(), retryCount); + abfsBackoffMetrics.setMaxRetryCount(maxRetryCount); updateCount(retryCount); } else { - abfsBackoffMetrics.incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING); + abfsBackoffMetrics.incrementNumberOfRequestsSucceededWithoutRetrying(); } } } @@ -405,12 +404,12 @@ private boolean executeHttpOperation(final int retryCount, AzureServiceErrorCode.INGRESS_OVER_ACCOUNT_LIMIT) || serviceErrorCode.equals( AzureServiceErrorCode.EGRESS_OVER_ACCOUNT_LIMIT)) { - abfsBackoffMetrics.incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS); + abfsBackoffMetrics.incrementNumberOfBandwidthThrottledRequests(); } else if (serviceErrorCode.equals( AzureServiceErrorCode.TPS_OVER_ACCOUNT_LIMIT)) { - abfsBackoffMetrics.incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_IOPS_THROTTLED_REQUESTS); + abfsBackoffMetrics.incrementNumberOfIOPSThrottledRequests(); } else { - abfsBackoffMetrics.incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_OTHER_THROTTLED_REQUESTS); + abfsBackoffMetrics.incrementNumberOfOtherThrottledRequests(); } } } @@ -456,7 +455,7 @@ private boolean executeHttpOperation(final int retryCount, } if (abfsBackoffMetrics != null) { synchronized (this) { - abfsBackoffMetrics.incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_NETWORK_FAILED_REQUESTS); + abfsBackoffMetrics.incrementNumberOfNetworkFailedRequests(); } } if (!retryPolicy.shouldRetry(retryCount, -1)) { @@ -471,7 +470,7 @@ private boolean executeHttpOperation(final int retryCount, } if (abfsBackoffMetrics != null) { synchronized (this) { - abfsBackoffMetrics.incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_NETWORK_FAILED_REQUESTS); + abfsBackoffMetrics.incrementNumberOfNetworkFailedRequests(); } } failureReason = RetryReason.getAbbreviation(ex, -1, ""); @@ -601,7 +600,7 @@ private void incrementCounter(AbfsStatistic statistic, long value) { */ private void updateCount(int retryCount){ String retryCounter = getKey(retryCount); - abfsBackoffMetrics.incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED, retryCounter); + abfsBackoffMetrics.incrementNumberOfRequestsSucceeded(retryCounter); } /** @@ -615,13 +614,13 @@ private void updateCount(int retryCount){ private void updateBackoffTimeMetrics(int retryCount, long sleepDuration) { synchronized (this) { String retryCounter = getKey(retryCount); - long minBackoffTime = Math.min(abfsBackoffMetrics.getCounter(AbfsBackoffMetricsEnum.MIN_BACK_OFF, retryCounter), sleepDuration); - long maxBackoffForTime = Math.max(abfsBackoffMetrics.getCounter(AbfsBackoffMetricsEnum.MAX_BACK_OFF, retryCounter), sleepDuration); - long totalBackoffTime = abfsBackoffMetrics.getCounter(AbfsBackoffMetricsEnum.TOTAL_BACK_OFF, retryCounter) + sleepDuration; - abfsBackoffMetrics.incrementCounter(AbfsBackoffMetricsEnum.TOTAL_REQUESTS, retryCounter); - abfsBackoffMetrics.setCounter(AbfsBackoffMetricsEnum.MIN_BACK_OFF, retryCounter, minBackoffTime); - abfsBackoffMetrics.setCounter(AbfsBackoffMetricsEnum.MAX_BACK_OFF, retryCounter, maxBackoffForTime); - abfsBackoffMetrics.setCounter(AbfsBackoffMetricsEnum.TOTAL_BACK_OFF, retryCounter, totalBackoffTime); + long minBackoffTime = Math.min(abfsBackoffMetrics.getMinBackoff(retryCounter), sleepDuration); + long maxBackoffForTime = Math.max(abfsBackoffMetrics.getMaxBackoff(retryCounter), sleepDuration); + long totalBackoffTime = abfsBackoffMetrics.getTotalBackoff(retryCounter) + sleepDuration; + abfsBackoffMetrics.incrementTotalRequests(retryCounter); + abfsBackoffMetrics.setMinBackoff(retryCounter, minBackoffTime); + abfsBackoffMetrics.setMaxBackoff(retryCounter, maxBackoffForTime); + abfsBackoffMetrics.setTotalBackoff(retryCounter, totalBackoffTime); } } diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsRestOperation.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsRestOperation.java index dee9b7a6a00fc..e99d05d41ca60 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsRestOperation.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsRestOperation.java @@ -21,7 +21,6 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.azurebfs.AbfsConfiguration; -import org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum; import org.apache.hadoop.fs.azurebfs.utils.MetricFormat; import org.junit.Test; import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.HTTP_METHOD_DELETE; @@ -74,7 +73,7 @@ public void testBackoffRetryMetrics() throws Exception { // For retry count greater than the max configured value, the request should fail. Assert.assertEquals("Number of failed requests does not match expected value.", - "3", String.valueOf(testClient.getAbfsCounters().getAbfsBackoffMetrics().getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_FAILED))); + "3", String.valueOf(testClient.getAbfsCounters().getAbfsBackoffMetrics().getNumberOfRequestsFailed())); // Close the AzureBlobFileSystem. fs.close(); From 7c6e0ffdfd93d4eb38474546368ec2e055541c9a Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Tue, 1 Oct 2024 03:08:00 -0700 Subject: [PATCH 05/32] Move AbfsBackoffMetrics to service directory --- .../org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java | 1 + .../fs/azurebfs/{ => services}/AbfsBackoffMetrics.java | 8 ++++---- .../apache/hadoop/fs/azurebfs/services/AbfsCounters.java | 1 - .../hadoop/fs/azurebfs/services/AbfsRestOperation.java | 2 -- 4 files changed, 5 insertions(+), 7 deletions(-) rename hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/{ => services}/AbfsBackoffMetrics.java (99%) diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java index c4d3e05cdb25d..4efe943712418 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java @@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicLong; import org.apache.hadoop.classification.VisibleForTesting; +import org.apache.hadoop.fs.azurebfs.services.AbfsBackoffMetrics; import org.apache.hadoop.fs.azurebfs.services.AbfsCounters; import org.apache.hadoop.fs.azurebfs.services.AbfsReadFooterMetrics; import org.apache.hadoop.fs.azurebfs.utils.MetricFormat; diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java similarity index 99% rename from hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsBackoffMetrics.java rename to hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java index 2cec974b47e2c..5f85e50e84f2c 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsBackoffMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java @@ -16,15 +16,15 @@ * limitations under the License. */ -package org.apache.hadoop.fs.azurebfs; +package org.apache.hadoop.fs.azurebfs.services; + +import java.util.Arrays; +import java.util.stream.Stream; import org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum; import org.apache.hadoop.fs.azurebfs.statistics.AbstractAbfsStatisticsSource; import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; -import java.util.Arrays; -import java.util.stream.Stream; - import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.HUNDRED; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.THOUSAND; import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.RETRY; diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsCounters.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsCounters.java index 65e5fa29a138b..0a94b66cbafe1 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsCounters.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsCounters.java @@ -25,7 +25,6 @@ import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; -import org.apache.hadoop.fs.azurebfs.AbfsBackoffMetrics; import org.apache.hadoop.fs.azurebfs.AbfsStatistic; import org.apache.hadoop.fs.azurebfs.utils.MetricFormat; import org.apache.hadoop.fs.statistics.DurationTracker; diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java index 5da92045406ea..c967abeb3fd89 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java @@ -43,8 +43,6 @@ import org.apache.hadoop.fs.azurebfs.utils.TracingContext; import org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding; import org.apache.hadoop.fs.azurebfs.contracts.services.AzureServiceErrorCode; -import java.util.Map; -import org.apache.hadoop.fs.azurebfs.AbfsBackoffMetrics; import org.apache.http.impl.execchain.RequestAbortedException; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.ZERO; From b7f7fbeb37f0643dc3edf15b8465c3df86def715 Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Thu, 17 Oct 2024 01:51:25 -0700 Subject: [PATCH 06/32] Hadoop-19311 Abfs Backoff and read footer metrics using IOStatistics --- .../hadoop/fs/azurebfs/AbfsCountersImpl.java | 9 +- .../azurebfs/constants/MetricsConstants.java | 5 + .../enums/AbfsReadFooterMetricsEnum.java | 37 + .../azurebfs/services/AbfsBackoffMetrics.java | 80 +- .../services/AbfsReadFooterMetrics.java | 733 ++++++------------ .../AbstractAbfsStatisticsSource.java | 24 +- .../azurebfs/ITestAbfsReadFooterMetrics.java | 34 +- .../services/TestAbfsReadFooterMetrics.java | 21 + 8 files changed, 362 insertions(+), 581 deletions(-) create mode 100644 hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java create mode 100644 hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java index 4efe943712418..2c67feb162cf5 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java @@ -331,12 +331,9 @@ public String toString() { } } if (abfsReadFooterMetrics != null) { - Map metricsMap = getAbfsReadFooterMetrics().getMetricsMap(); - if (metricsMap != null && !(metricsMap.isEmpty())) { - String readFooterMetric = getAbfsReadFooterMetrics().toString(); - if (!readFooterMetric.equals("")) { - metric += "#FO:" + getAbfsReadFooterMetrics().toString(); - } + long totalFiles = getAbfsReadFooterMetrics().getTotalFiles(); + if (totalFiles > 0) { + metric += "#FO:" + getAbfsReadFooterMetrics().toString(); } } return metric; diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java index 5ab58628c0d9c..caf3623b3451d 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java @@ -4,7 +4,12 @@ import java.util.List; public final class MetricsConstants { + public static final String COLON = ":"; public static final String RETRY = "RETRY"; public static final String BASE = "BASE"; public static final List RETRY_LIST = Arrays.asList("1", "2", "3", "4", "5_15", "15_25", "25AndAbove"); + public static final String COUNTER = "COUNTER"; + public static final String GAUGE = "GAUGE"; + public static final String PARQUET = "PARQUET"; + public static final String NON_PARQUET = "NON_PARQUET"; } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java new file mode 100644 index 0000000000000..f52636d508835 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java @@ -0,0 +1,37 @@ +package org.apache.hadoop.fs.azurebfs.enums; + +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.COUNTER; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.GAUGE; + +public enum AbfsReadFooterMetricsEnum { + TOTAL_FILES("totalFiles", "Total files in a file system", COUNTER), + FILE_LENGTH("fileLength", "File length", GAUGE), + SIZE_READ_BY_FIRST_READ("sizeReadByFirstRead", "Size read by first read", GAUGE), + OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ("offsetDiffBetweenFirstAndSecondRead", "Offset difference between first and second read", GAUGE), + READ_LEN_REQUESTED("readLenRequested", "Read length requested", GAUGE), + READ_COUNT("readCount", "Read count", COUNTER), + FIRST_OFFSET_DIFF("firstOffsetDiff", "First offset difference", GAUGE), + SECOND_OFFSET_DIFF("secondOffsetDiff", "Second offset difference", GAUGE); + + private final String name; + private final String description; + private final String type; + + AbfsReadFooterMetricsEnum(String name, String description, String type) { + this.name = name; + this.description = description; + this.type = type; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getType() { + return type; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java index 5f85e50e84f2c..ae2cde87a7230 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java @@ -29,6 +29,7 @@ import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.THOUSAND; import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.RETRY; import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.RETRY_LIST; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.COLON; import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.iostatisticsStore; public class AbfsBackoffMetrics extends AbstractAbfsStatisticsSource { @@ -42,19 +43,15 @@ public AbfsBackoffMetrics() { private String[] getCountersName() { return Arrays.stream(AbfsBackoffMetricsEnum.values()) - .flatMap(backoffMetricsEnum -> { - if (RETRY.equals(backoffMetricsEnum.getType())) { - return RETRY_LIST.stream() - .map(retryCount -> retryCount + ":" + backoffMetricsEnum.getName()); - } else { - return Stream.of(backoffMetricsEnum.getName()); - } - }) + .flatMap(backoffMetricsEnum -> + RETRY.equals(backoffMetricsEnum.getType()) ? + RETRY_LIST.stream().map(retryCount -> retryCount + COLON + backoffMetricsEnum.getName()) : + Stream.of(backoffMetricsEnum.getName())) .toArray(String[]::new); } public void incrementCounter(AbfsBackoffMetricsEnum metric, String retryCount, long value) { - incCounter(retryCount + ":" + metric.getName(), value); + incCounter(retryCount + COLON + metric.getName(), value); } public void incrementCounter(AbfsBackoffMetricsEnum metric, String retryCount) { @@ -70,7 +67,7 @@ public void incrementCounter(AbfsBackoffMetricsEnum metric) { } public Long getCounter(AbfsBackoffMetricsEnum metric, String retryCount) { - return lookupCounterValue(retryCount + ":" + metric.getName()); + return lookupCounterValue(retryCount + COLON + metric.getName()); } public Long getCounter(AbfsBackoffMetricsEnum metric) { @@ -78,7 +75,7 @@ public Long getCounter(AbfsBackoffMetricsEnum metric) { } public void setCounter(AbfsBackoffMetricsEnum metric, String retryCount, long value) { - setCounterValue(retryCount + ":" + metric.getName(), value); + setCounterValue(retryCount + COLON + metric.getName(), value); } public void setCounter(AbfsBackoffMetricsEnum metric, long value) { @@ -210,51 +207,30 @@ public String toString() { long totalRequestsThrottled = getNumberOfBandwidthThrottledRequests() + getNumberOfIOPSThrottledRequests() + getNumberOfOtherThrottledRequests(); - double percentageOfRequestsThrottled = - ((double) totalRequestsThrottled / getTotalNumberOfRequests()) * HUNDRED; - for (String retryCount: RETRY_LIST) { - metricString.append("$RCTSI$_").append(retryCount) - .append("R_").append("=") - .append(getNumberOfRequestsSucceeded(retryCount)); + double percentageOfRequestsThrottled = ((double) totalRequestsThrottled / getTotalNumberOfRequests()) * HUNDRED; + + for (String retryCount : RETRY_LIST) { long totalRequests = getTotalRequests(retryCount); + metricString.append("$RCTSI$_").append(retryCount).append("R=").append(getNumberOfRequestsSucceeded(retryCount)); if (totalRequests > 0) { - metricString.append("$MMA$_").append(retryCount) - .append("R_").append("=") - .append(String.format("%.3f", - (double) getMinBackoff(retryCount) / THOUSAND)) - .append("s") - .append(String.format("%.3f", - (double) getMaxBackoff(retryCount) / THOUSAND)) - .append("s") - .append(String.format("%.3f", - ((double) getTotalBackoff(retryCount) / totalRequests) - / THOUSAND)) - .append("s"); + metricString.append("$MMA$_").append(retryCount).append("R=") + .append(String.format("%.3f", (double) getMinBackoff(retryCount) / THOUSAND)).append("s") + .append(String.format("%.3f", (double) getMaxBackoff(retryCount) / THOUSAND)).append("s") + .append(String.format("%.3f", (double) getTotalBackoff(retryCount) / totalRequests / THOUSAND)).append("s"); } else { - metricString.append("$MMA$_").append(retryCount) - .append("R_").append("=0s"); + metricString.append("$MMA$_").append(retryCount).append("R=0s"); } } - metricString.append("$BWT=") - .append(getNumberOfBandwidthThrottledRequests()) - .append("$IT=") - .append(getNumberOfIOPSThrottledRequests()) - .append("$OT=") - .append(getNumberOfOtherThrottledRequests()) - .append("$RT=") - .append(String.format("%.3f", percentageOfRequestsThrottled)) - .append("$NFR=") - .append(getNumberOfNetworkFailedRequests()) - .append("$TRNR=") - .append(getNumberOfRequestsSucceededWithoutRetrying()) - .append("$TRF=") - .append(getNumberOfRequestsFailed()) - .append("$TR=") - .append(getTotalNumberOfRequests()) - .append("$MRC=") - .append(getMaxRetryCount()); - - return metricString + ""; + metricString.append("$BWT=").append(getNumberOfBandwidthThrottledRequests()) + .append("$IT=").append(getNumberOfIOPSThrottledRequests()) + .append("$OT=").append(getNumberOfOtherThrottledRequests()) + .append("$RT=").append(String.format("%.3f", percentageOfRequestsThrottled)) + .append("$NFR=").append(getNumberOfNetworkFailedRequests()) + .append("$TRNR=").append(getNumberOfRequestsSucceededWithoutRetrying()) + .append("$TRF=").append(getNumberOfRequestsFailed()) + .append("$TR=").append(getTotalNumberOfRequests()) + .append("$MRC=").append(getMaxRetryCount()); + + return metricString.toString(); } } - diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java index 5abb97cd9ce03..f160ea99e5938 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java @@ -17,533 +17,268 @@ */ package org.apache.hadoop.fs.azurebfs.services; -import java.util.ArrayList; -import java.util.List; import java.util.Map; -import java.util.StringJoiner; -import java.util.concurrent.ConcurrentSkipListMap; +import java.util.HashMap; +import java.util.Arrays; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Stream; + +import org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum; +import org.apache.hadoop.fs.azurebfs.statistics.AbstractAbfsStatisticsSource; +import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.ONE_KB; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.COLON; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.COUNTER; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.GAUGE; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.PARQUET; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.NON_PARQUET; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.TOTAL_FILES; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.FILE_LENGTH; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.SIZE_READ_BY_FIRST_READ; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.READ_LEN_REQUESTED; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.READ_COUNT; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.FIRST_OFFSET_DIFF; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.SECOND_OFFSET_DIFF; +import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.iostatisticsStore; + +public class AbfsReadFooterMetrics extends AbstractAbfsStatisticsSource { + private static final String FOOTER_LENGTH = "20"; + + private static class CheckFileType { + private final AtomicBoolean collectMetrics; + private final AtomicBoolean collectMetricsForNextRead; + private final AtomicBoolean collectLenMetrics; + private final AtomicLong readCount; + private final AtomicLong offsetOfFirstRead; + private Boolean isParquet = null; + private String sizeReadByFirstRead; + private String offsetDiffBetweenFirstAndSecondRead; + + private CheckFileType() { + collectMetrics = new AtomicBoolean(false); + collectMetricsForNextRead = new AtomicBoolean(false); + collectLenMetrics = new AtomicBoolean(false); + readCount = new AtomicLong(0); + offsetOfFirstRead = new AtomicLong(0); + } + + private boolean isParquet() { + if (isParquet != null) return isParquet; + isParquet = collectMetrics.get() && readCount.get() >= 2 && + haveEqualValues(sizeReadByFirstRead) && + haveEqualValues(offsetDiffBetweenFirstAndSecondRead); + return isParquet; + } + + private boolean haveEqualValues(String value) { + String[] parts = value.split("_"); + return parts.length == 2 && parts[0].equals(parts[1]); + } + + private void incrementReadCount() { + readCount.incrementAndGet(); + } + + private long getReadCount() { + return readCount.get(); + } + + private void setCollectMetrics(boolean collect) { + collectMetrics.set(collect); + } + + private boolean getCollectMetrics() { + return collectMetrics.get(); + } + + private void setCollectMetricsForNextRead(boolean collect) { + collectMetricsForNextRead.set(collect); + } + + private boolean getCollectMetricsForNextRead() { + return collectMetricsForNextRead.get(); + } + + private boolean getCollectLenMetrics() { + return collectLenMetrics.get(); + } + + private void setCollectLenMetrics(boolean collect) { + collectLenMetrics.set(collect); + } + + private void setOffsetOfFirstRead(long offset) { + offsetOfFirstRead.set(offset); + } + + private long getOffsetOfFirstRead() { + return offsetOfFirstRead.get(); + } + + private void setSizeReadByFirstRead(String size) { + sizeReadByFirstRead = size; + } + + private String getSizeReadByFirstRead() { + return sizeReadByFirstRead; + } + + private void setOffsetDiffBetweenFirstAndSecondRead(String offsetDiff) { + offsetDiffBetweenFirstAndSecondRead = offsetDiff; + } -public class AbfsReadFooterMetrics { - private final AtomicBoolean isParquetFile; - private final AtomicBoolean isParquetEvaluated; - private final AtomicBoolean isLenUpdated; - private String sizeReadByFirstRead; - private String offsetDiffBetweenFirstAndSecondRead; - private final AtomicLong fileLength; - private double avgFileLength; - private double avgReadLenRequested; - private final AtomicBoolean collectMetrics; - private final AtomicBoolean collectMetricsForNextRead; - private final AtomicBoolean collectLenMetrics; - private final AtomicLong dataLenRequested; - private final AtomicLong offsetOfFirstRead; - private final AtomicInteger readCount; - private final ConcurrentSkipListMap metricsMap; - private static final String FOOTER_LENGTH = "20"; - - public AbfsReadFooterMetrics() { - this.isParquetFile = new AtomicBoolean(false); - this.isParquetEvaluated = new AtomicBoolean(false); - this.isLenUpdated = new AtomicBoolean(false); - this.fileLength = new AtomicLong(); - this.readCount = new AtomicInteger(0); - this.offsetOfFirstRead = new AtomicLong(); - this.collectMetrics = new AtomicBoolean(false); - this.collectMetricsForNextRead = new AtomicBoolean(false); - this.collectLenMetrics = new AtomicBoolean(false); - this.dataLenRequested = new AtomicLong(0); - this.metricsMap = new ConcurrentSkipListMap<>(); - } - - public Map getMetricsMap() { - return metricsMap; - } - - private boolean getIsParquetFile() { - return isParquetFile.get(); - } - - public void setIsParquetFile(boolean isParquetFile) { - this.isParquetFile.set(isParquetFile); - } - - private String getSizeReadByFirstRead() { - return sizeReadByFirstRead; - } - - public void setSizeReadByFirstRead(final String sizeReadByFirstRead) { - this.sizeReadByFirstRead = sizeReadByFirstRead; - } - - private String getOffsetDiffBetweenFirstAndSecondRead() { - return offsetDiffBetweenFirstAndSecondRead; - } - - public void setOffsetDiffBetweenFirstAndSecondRead(final String offsetDiffBetweenFirstAndSecondRead) { - this.offsetDiffBetweenFirstAndSecondRead - = offsetDiffBetweenFirstAndSecondRead; - } - - private long getFileLength() { - return fileLength.get(); - } - - private void setFileLength(long fileLength) { - this.fileLength.set(fileLength); - } - - private double getAvgFileLength() { - return avgFileLength; - } - - public void setAvgFileLength(final double avgFileLength) { - this.avgFileLength = avgFileLength; - } - - private double getAvgReadLenRequested() { - return avgReadLenRequested; - } - - public void setAvgReadLenRequested(final double avgReadLenRequested) { - this.avgReadLenRequested = avgReadLenRequested; - } - - private boolean getCollectMetricsForNextRead() { - return collectMetricsForNextRead.get(); - } - - private void setCollectMetricsForNextRead(boolean collectMetricsForNextRead) { - this.collectMetricsForNextRead.set(collectMetricsForNextRead); - } - - private long getOffsetOfFirstRead() { - return offsetOfFirstRead.get(); - } - - private void setOffsetOfFirstRead(long offsetOfFirstRead) { - this.offsetOfFirstRead.set(offsetOfFirstRead); - } - - private int getReadCount() { - return readCount.get(); - } - - private void setReadCount(int readCount) { - this.readCount.set(readCount); - } - - private int incrementReadCount() { - this.readCount.incrementAndGet(); - return getReadCount(); - } - - private boolean getCollectLenMetrics() { - return collectLenMetrics.get(); - } - - private void setCollectLenMetrics(boolean collectLenMetrics) { - this.collectLenMetrics.set(collectLenMetrics); - - } - - private long getDataLenRequested() { - return dataLenRequested.get(); - } - - private void setDataLenRequested(long dataLenRequested) { - this.dataLenRequested.set(dataLenRequested); - } - - private void updateDataLenRequested(long dataLenRequested){ - this.dataLenRequested.addAndGet(dataLenRequested); - } - - private boolean getCollectMetrics() { - return collectMetrics.get(); - } - - private void setCollectMetrics(boolean collectMetrics) { - this.collectMetrics.set(collectMetrics); - } - - private boolean getIsParquetEvaluated() { - return isParquetEvaluated.get(); - } - - private void setIsParquetEvaluated(boolean isParquetEvaluated) { - this.isParquetEvaluated.set(isParquetEvaluated); - } - - private boolean getIsLenUpdated() { - return isLenUpdated.get(); - } - - private void setIsLenUpdated(boolean isLenUpdated) { - this.isLenUpdated.set(isLenUpdated); - } - - /** - * Updates the metrics map with an entry for the specified file if it doesn't already exist. - * - * @param filePathIdentifier The unique identifier for the file. - */ - public void updateMap(String filePathIdentifier) { - // If the file is not already in the metrics map, add it with a new AbfsReadFooterMetrics object. - metricsMap.computeIfAbsent(filePathIdentifier, key -> new AbfsReadFooterMetrics()); - } - - /** - * Checks and updates metrics for a specific file identified by filePathIdentifier. - * If the metrics do not exist for the file, they are initialized. - * - * @param filePathIdentifier The unique identifier for the file. - * @param len The length of the read operation. - * @param contentLength The total content length of the file. - * @param nextReadPos The position of the next read operation. - */ - public void checkMetricUpdate(final String filePathIdentifier, final int len, final long contentLength, - final long nextReadPos) { - AbfsReadFooterMetrics readFooterMetrics = metricsMap.computeIfAbsent( - filePathIdentifier, key -> new AbfsReadFooterMetrics()); - if (readFooterMetrics.getReadCount() == 0 - || (readFooterMetrics.getReadCount() >= 1 - && readFooterMetrics.getCollectMetrics())) { - updateMetrics(filePathIdentifier, len, contentLength, nextReadPos); + private String getOffsetDiffBetweenFirstAndSecondRead() { + return offsetDiffBetweenFirstAndSecondRead; + } } - } - - /** - * Updates metrics for a specific file identified by filePathIdentifier. - * - * @param filePathIdentifier The unique identifier for the file. - * @param len The length of the read operation. - * @param contentLength The total content length of the file. - * @param nextReadPos The position of the next read operation. - */ - private void updateMetrics(final String filePathIdentifier, final int len, final long contentLength, - final long nextReadPos) { - AbfsReadFooterMetrics readFooterMetrics = metricsMap.get(filePathIdentifier); - - // Create a new AbfsReadFooterMetrics object if it doesn't exist in the metricsMap. - if (readFooterMetrics == null) { - readFooterMetrics = new AbfsReadFooterMetrics(); - metricsMap.put(filePathIdentifier, readFooterMetrics); + + private final Map checkFileMap = new HashMap<>(); + + public AbfsReadFooterMetrics() { + IOStatisticsStore ioStatisticsStore = iostatisticsStore() + .withCounters(getMetricNames(COUNTER)) + .withGauges(getMetricNames(GAUGE)) + .build(); + setIOStatistics(ioStatisticsStore); } - int readCount; - synchronized (this) { - readCount = readFooterMetrics.incrementReadCount(); + private String[] getMetricNames(String type) { + return Arrays.stream(AbfsReadFooterMetricsEnum.values()) + .filter(metric -> metric.getType().equals(type)) + .flatMap(metric -> Stream.of(PARQUET + COLON + metric.getName(), NON_PARQUET + COLON + metric.getName())) + .toArray(String[]::new); } - if (readCount == 1) { - // Update metrics for the first read. - updateMetricsOnFirstRead(readFooterMetrics, nextReadPos, len, contentLength); + private long getMetricValue(String fileType, AbfsReadFooterMetricsEnum metric) { + switch (metric.getType()) { + case COUNTER: + return lookupCounterValue(fileType + COLON + metric.getName()); + case GAUGE: + return lookupGaugeValue(fileType + COLON + metric.getName()); + default: + return 0; + } } - synchronized (this) { - if (readFooterMetrics.getCollectLenMetrics()) { - readFooterMetrics.updateDataLenRequested(len); - } + public void updateMetric(String fileType, AbfsReadFooterMetricsEnum metric, long value) { + updateGauge(fileType + COLON + metric.getName(), value); } - if (readCount == 2) { - // Update metrics for the second read. - updateMetricsOnSecondRead(readFooterMetrics, nextReadPos, len); + public void incrementMetricValue(String fileType, AbfsReadFooterMetricsEnum metricName) { + incCounter(fileType + COLON + metricName.getName()); } - } - - /** - * Updates metrics for the first read operation. - * - * @param readFooterMetrics The metrics object to update. - * @param nextReadPos The position of the next read operation. - * @param len The length of the read operation. - * @param contentLength The total content length of the file. - */ - private void updateMetricsOnFirstRead(AbfsReadFooterMetrics readFooterMetrics, long nextReadPos, int len, long contentLength) { - if (nextReadPos >= contentLength - (long) Integer.parseInt(FOOTER_LENGTH) * ONE_KB) { - readFooterMetrics.setCollectMetrics(true); - readFooterMetrics.setCollectMetricsForNextRead(true); - readFooterMetrics.setOffsetOfFirstRead(nextReadPos); - readFooterMetrics.setSizeReadByFirstRead(len + "_" + Math.abs(contentLength - nextReadPos)); - readFooterMetrics.setFileLength(contentLength); + + public void incrementMetricValue(String fileType, AbfsReadFooterMetricsEnum metricName, long value) { + incCounter(fileType + COLON + metricName.getName(), value); } - } - - /** - * Updates metrics for the second read operation. - * - * @param readFooterMetrics The metrics object to update. - * @param nextReadPos The position of the next read operation. - * @param len The length of the read operation. - */ - private void updateMetricsOnSecondRead(AbfsReadFooterMetrics readFooterMetrics, long nextReadPos, int len) { - if (readFooterMetrics.getCollectMetricsForNextRead()) { - long offsetDiff = Math.abs(nextReadPos - readFooterMetrics.getOffsetOfFirstRead()); - readFooterMetrics.setOffsetDiffBetweenFirstAndSecondRead(len + "_" + offsetDiff); - readFooterMetrics.setCollectLenMetrics(true); + + public Long getTotalFiles() { + return getMetricValue(PARQUET, TOTAL_FILES) + getMetricValue(NON_PARQUET, TOTAL_FILES); } - } - - - /** - * Check if the given file should be marked as a Parquet file. - * - * @param metrics The metrics to evaluate. - * @return True if the file meet the criteria for being marked as a Parquet file, false otherwise. - */ - private boolean shouldMarkAsParquet(AbfsReadFooterMetrics metrics) { - return metrics.getCollectMetrics() - && metrics.getReadCount() >= 2 - && !metrics.getIsParquetEvaluated() - && haveEqualValues(metrics.getSizeReadByFirstRead()) - && haveEqualValues(metrics.getOffsetDiffBetweenFirstAndSecondRead()); - } - - /** - * Check if two values are equal, considering they are in the format "value1_value2". - * - * @param value The value to check. - * @return True if the two parts of the value are equal, false otherwise. - */ - private boolean haveEqualValues(String value) { - String[] parts = value.split("_"); - return parts.length == 2 && parts[0].equals(parts[1]); - } - - /** - * Mark the given metrics as a Parquet file and update related values. - * - * @param metrics The metrics to mark as Parquet. - */ - private void markAsParquet(AbfsReadFooterMetrics metrics) { - metrics.setIsParquetFile(true); - String[] parts = metrics.getSizeReadByFirstRead().split("_"); - metrics.setSizeReadByFirstRead(parts[0]); - parts = metrics.getOffsetDiffBetweenFirstAndSecondRead().split("_"); - metrics.setOffsetDiffBetweenFirstAndSecondRead(parts[0]); - metrics.setIsParquetEvaluated(true); - } - - /** - * Check each metric in the provided map and mark them as Parquet files if they meet the criteria. - * - * @param metricsMap The map containing metrics to evaluate. - */ - public void checkIsParquet(Map metricsMap) { - for (Map.Entry entry : metricsMap.entrySet()) { - AbfsReadFooterMetrics readFooterMetrics = entry.getValue(); - if (shouldMarkAsParquet(readFooterMetrics)) { - markAsParquet(readFooterMetrics); - metricsMap.replace(entry.getKey(), readFooterMetrics); - } + + public void updateMap(String filePathIdentifier) { + checkFileMap.computeIfAbsent(filePathIdentifier, key -> new CheckFileType()); } - } - - /** - * Updates the average read length requested for metrics of all files in the metrics map. - * If the metrics indicate that the update is needed, it calculates the average read length and updates the metrics. - * - * @param metricsMap A map containing metrics for different files with unique identifiers. - */ - private void updateLenRequested(Map metricsMap) { - for (AbfsReadFooterMetrics readFooterMetrics : metricsMap.values()) { - if (shouldUpdateLenRequested(readFooterMetrics)) { - int readReqCount = readFooterMetrics.getReadCount() - 2; - readFooterMetrics.setAvgReadLenRequested( - (double) readFooterMetrics.getDataLenRequested() / readReqCount); - readFooterMetrics.setIsLenUpdated(true); - } + + public void checkMetricUpdate(final String filePathIdentifier, final int len, final long contentLength, final long nextReadPos) { + CheckFileType checkFileType = checkFileMap.computeIfAbsent(filePathIdentifier, key -> new CheckFileType()); + if (checkFileType.getReadCount() == 0 || (checkFileType.getReadCount() >= 1 && checkFileType.getCollectMetrics())) { + updateMetrics(checkFileType, len, contentLength, nextReadPos); + } } - } - - /** - * Checks whether the average read length requested should be updated for the given metrics. - * - * The method returns true if the following conditions are met: - * - Metrics collection is enabled. - * - The number of read counts is greater than 2. - * - The average read length has not been updated previously. - * - * @param readFooterMetrics The metrics object to evaluate. - * @return True if the average read length should be updated, false otherwise. - */ - private boolean shouldUpdateLenRequested(AbfsReadFooterMetrics readFooterMetrics) { - return readFooterMetrics.getCollectMetrics() - && readFooterMetrics.getReadCount() > 2 - && !readFooterMetrics.getIsLenUpdated(); - } - - /** - * Calculates the average metrics from a list of AbfsReadFooterMetrics and sets the values in the provided 'avgParquetReadFooterMetrics' object. - * - * @param isParquetList The list of AbfsReadFooterMetrics to compute the averages from. - * @param avgParquetReadFooterMetrics The target AbfsReadFooterMetrics object to store the computed average values. - * - * This method calculates various average metrics from the provided list and sets them in the 'avgParquetReadFooterMetrics' object. - * The metrics include: - * - Size read by the first read - * - Offset difference between the first and second read - * - Average file length - * - Average requested read length - */ - private void getParquetReadFooterMetricsAverage(List isParquetList, - AbfsReadFooterMetrics avgParquetReadFooterMetrics){ - avgParquetReadFooterMetrics.setSizeReadByFirstRead( - String.format("%.3f", isParquetList.stream() - .map(AbfsReadFooterMetrics::getSizeReadByFirstRead).mapToDouble( - Double::parseDouble).average().orElse(0.0))); - avgParquetReadFooterMetrics.setOffsetDiffBetweenFirstAndSecondRead( - String.format("%.3f", isParquetList.stream() - .map(AbfsReadFooterMetrics::getOffsetDiffBetweenFirstAndSecondRead) - .mapToDouble(Double::parseDouble).average().orElse(0.0))); - avgParquetReadFooterMetrics.setAvgFileLength(isParquetList.stream() - .mapToDouble(AbfsReadFooterMetrics::getFileLength).average().orElse(0.0)); - avgParquetReadFooterMetrics.setAvgReadLenRequested(isParquetList.stream(). - map(AbfsReadFooterMetrics::getAvgReadLenRequested). - mapToDouble(Double::doubleValue).average().orElse(0.0)); - } - - /** - * Calculates the average metrics from a list of non-Parquet AbfsReadFooterMetrics instances. - * - * This method takes a list of AbfsReadFooterMetrics representing non-Parquet reads and calculates - * the average values for the size read by the first read and the offset difference between the first - * and second read. The averages are then set in the provided AbfsReadFooterMetrics instance. - * - * @param isNonParquetList A list of AbfsReadFooterMetrics instances representing non-Parquet reads. - * @param avgNonParquetReadFooterMetrics The AbfsReadFooterMetrics instance to store the calculated averages. - * It is assumed that the size of the list is at least 1, and the first - * element of the list is used to determine the size of arrays. - * The instance is modified in-place with the calculated averages. - * - * - **/ - private void getNonParquetReadFooterMetricsAverage(List isNonParquetList, - AbfsReadFooterMetrics avgNonParquetReadFooterMetrics) { - int size = isNonParquetList.get(0).getSizeReadByFirstRead().split("_").length; - double[] store = new double[2 * size]; - // Calculating sum of individual values - isNonParquetList.forEach(abfsReadFooterMetrics -> { - String[] firstReadSize = abfsReadFooterMetrics.getSizeReadByFirstRead().split("_"); - String[] offDiffFirstSecondRead = abfsReadFooterMetrics.getOffsetDiffBetweenFirstAndSecondRead().split("_"); - - for (int i = 0; i < firstReadSize.length; i++) { - store[i] += Long.parseLong(firstReadSize[i]); - store[i + size] += Long.parseLong(offDiffFirstSecondRead[i]); - } - }); - - // Calculating averages and creating formatted strings - StringJoiner firstReadSize = new StringJoiner("_"); - StringJoiner offDiffFirstSecondRead = new StringJoiner("_"); - - for (int j = 0; j < size; j++) { - firstReadSize.add(String.format("%.3f", store[j] / isNonParquetList.size())); - offDiffFirstSecondRead.add(String.format("%.3f", store[j + size] / isNonParquetList.size())); + + private void updateMetrics(CheckFileType checkFileType, int len, long contentLength, long nextReadPos) { + synchronized (this) { + checkFileType.incrementReadCount(); + } + + long readCount = checkFileType.getReadCount(); + + if (readCount == 1) { + handleFirstRead(checkFileType, nextReadPos, len, contentLength); + } else if (readCount == 2) { + handleSecondRead(checkFileType, nextReadPos, len, contentLength); + } else { + handleFurtherRead(checkFileType, len); + } } - avgNonParquetReadFooterMetrics.setSizeReadByFirstRead(firstReadSize.toString()); - avgNonParquetReadFooterMetrics.setOffsetDiffBetweenFirstAndSecondRead(offDiffFirstSecondRead.toString()); - avgNonParquetReadFooterMetrics.setAvgFileLength(isNonParquetList.stream() - .mapToDouble(AbfsReadFooterMetrics::getFileLength).average().orElse(0.0)); - avgNonParquetReadFooterMetrics.setAvgReadLenRequested(isNonParquetList.stream() - .mapToDouble(AbfsReadFooterMetrics::getAvgReadLenRequested).average().orElse(0.0)); - } - - /* - Acronyms: - 1.FR :- First Read (In case of parquet we only maintain the size requested by application for - the first read, in case of non parquet we maintain a string separated by "_" delimiter where the first - substring represents the len requested for first read and the second substring represents the seek pointer difference from the - end of the file.) - 2.SR :- Second Read (In case of parquet we only maintain the size requested by application for - the second read, in case of non parquet we maintain a string separated by "_" delimiter where the first - substring represents the len requested for second read and the second substring represents the seek pointer difference from the - offset of the first read.) - 3.FL :- Total length of the file requested for read - */ - public String getReadFooterMetrics(AbfsReadFooterMetrics avgReadFooterMetrics) { - String readFooterMetric = ""; - if (avgReadFooterMetrics.getIsParquetFile()) { - readFooterMetric += "$Parquet:"; - } else { - readFooterMetric += "$NonParquet:"; + private void handleFirstRead(CheckFileType checkFileType, long nextReadPos, int len, long contentLength) { + if (nextReadPos >= contentLength - (long) Integer.parseInt(FOOTER_LENGTH) * ONE_KB) { + checkFileType.setCollectMetrics(true); + checkFileType.setCollectMetricsForNextRead(true); + checkFileType.setOffsetOfFirstRead(nextReadPos); + checkFileType.setSizeReadByFirstRead(len + "_" + Math.abs(contentLength - nextReadPos)); + } } - readFooterMetric += "$FR=" + avgReadFooterMetrics.getSizeReadByFirstRead() - + "$SR=" - + avgReadFooterMetrics.getOffsetDiffBetweenFirstAndSecondRead() - + "$FL=" + String.format("%.3f", - avgReadFooterMetrics.getAvgFileLength()) - + "$RL=" + String.format("%.3f", - avgReadFooterMetrics.getAvgReadLenRequested()); - return readFooterMetric; - } -/** - * Retrieves and aggregates read footer metrics for both Parquet and non-Parquet files from a list - * of AbfsReadFooterMetrics instances. The function calculates the average metrics separately for - * Parquet and non-Parquet files and returns a formatted string containing the aggregated metrics. - * - * @param readFooterMetricsList A list of AbfsReadFooterMetrics instances containing read footer metrics - * for both Parquet and non-Parquet files. - * - * @return A formatted string containing the aggregated read footer metrics for both Parquet and non-Parquet files. - * - **/ -private String getFooterMetrics(List readFooterMetricsList) { - List isParquetList = new ArrayList<>(); - List isNonParquetList = new ArrayList<>(); - for (AbfsReadFooterMetrics abfsReadFooterMetrics : readFooterMetricsList) { - if (abfsReadFooterMetrics.getIsParquetFile()) { - isParquetList.add(abfsReadFooterMetrics); - } else { - if (abfsReadFooterMetrics.getReadCount() >= 2) { - isNonParquetList.add(abfsReadFooterMetrics); - } + private void handleSecondRead(CheckFileType checkFileType, long nextReadPos, int len, long contentLength) { + if (checkFileType.getCollectMetricsForNextRead()) { + long offsetDiff = Math.abs(nextReadPos - checkFileType.getOffsetOfFirstRead()); + checkFileType.setOffsetDiffBetweenFirstAndSecondRead(len + "_" + offsetDiff); + checkFileType.setCollectLenMetrics(true); + updateMetricsData(checkFileType, len, contentLength); + } + } + + private void handleFurtherRead(CheckFileType checkFileType, int len) { + if (checkFileType.getCollectLenMetrics()) { + String fileType = checkFileType.isParquet() ? PARQUET : NON_PARQUET; + updateMetric(fileType, READ_LEN_REQUESTED, len); + incrementMetricValue(fileType, READ_COUNT); + } } - } - AbfsReadFooterMetrics avgParquetReadFooterMetrics = new AbfsReadFooterMetrics(); - AbfsReadFooterMetrics avgNonparquetReadFooterMetrics = new AbfsReadFooterMetrics(); - String readFooterMetric = ""; - if (!isParquetList.isEmpty()) { - avgParquetReadFooterMetrics.setIsParquetFile(true); - getParquetReadFooterMetricsAverage(isParquetList, avgParquetReadFooterMetrics); - readFooterMetric += getReadFooterMetrics(avgParquetReadFooterMetrics); - } - if (!isNonParquetList.isEmpty()) { - avgNonparquetReadFooterMetrics.setIsParquetFile(false); - getNonParquetReadFooterMetricsAverage(isNonParquetList, avgNonparquetReadFooterMetrics); - readFooterMetric += getReadFooterMetrics(avgNonparquetReadFooterMetrics); - } - return readFooterMetric; -} + private void updateMetricsData(CheckFileType checkFileType, int len, long contentLength) { + long sizeReadByFirstRead = Long.parseLong(checkFileType.getSizeReadByFirstRead().split("_")[0]); + long firstOffsetDiff = Long.parseLong(checkFileType.getSizeReadByFirstRead().split("_")[1]); + long secondOffsetDiff = Long.parseLong(checkFileType.getOffsetDiffBetweenFirstAndSecondRead().split("_")[1]); + String fileType = checkFileType.isParquet() ? PARQUET : NON_PARQUET; + + incrementMetricValue(fileType, READ_COUNT, 2); + updateMetric(fileType, READ_LEN_REQUESTED, len + sizeReadByFirstRead); + updateMetric(fileType, FILE_LENGTH, contentLength); + updateMetric(fileType, SIZE_READ_BY_FIRST_READ, sizeReadByFirstRead); + updateMetric(fileType, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ, len); + updateMetric(fileType, FIRST_OFFSET_DIFF, firstOffsetDiff); + updateMetric(fileType, SECOND_OFFSET_DIFF, secondOffsetDiff); + incrementMetricValue(fileType, TOTAL_FILES); + } + + public String getReadFooterMetrics(String fileType) { + StringBuilder readFooterMetric = new StringBuilder(); + appendMetrics(readFooterMetric, fileType); + return readFooterMetric.toString(); + } + + private void appendMetrics(StringBuilder metricBuilder, String fileType) { + long totalFiles = getMetricValue(fileType, TOTAL_FILES); + if (totalFiles <= 0) return; - @Override - public String toString() { - Map metricsMap = getMetricsMap(); - List readFooterMetricsList = new ArrayList<>(); - if (metricsMap != null && !(metricsMap.isEmpty())) { - checkIsParquet(metricsMap); - updateLenRequested(metricsMap); - for (Map.Entry entry : metricsMap.entrySet()) { - AbfsReadFooterMetrics abfsReadFooterMetrics = entry.getValue(); - if (abfsReadFooterMetrics.getCollectMetrics()) { - readFooterMetricsList.add(entry.getValue()); + long readCount = getMetricValue(fileType, READ_COUNT); + String sizeReadByFirstRead = String.format("%.3f",getMetricValue(fileType, SIZE_READ_BY_FIRST_READ) / (double) totalFiles); + String offsetDiffBetweenFirstAndSecondRead = String.format("%.3f",getMetricValue(fileType, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ) / (double) totalFiles); + + if (NON_PARQUET.equals(fileType)) { + sizeReadByFirstRead += "_" + String.format("%.3f",getMetricValue(fileType, FIRST_OFFSET_DIFF) / (double) totalFiles); + offsetDiffBetweenFirstAndSecondRead += "_" + String.format("%.3f",getMetricValue(fileType, SECOND_OFFSET_DIFF) / (double) totalFiles); } - } + + metricBuilder.append("$").append(fileType) + .append(":$FR=").append(sizeReadByFirstRead) + .append("$SR=").append(offsetDiffBetweenFirstAndSecondRead) + .append("$FL=").append(String.format("%.3f", getMetricValue(fileType, FILE_LENGTH) / (double) totalFiles)) + .append("$RL=").append(String.format("%.3f", getMetricValue(fileType, READ_LEN_REQUESTED) / (double) (readCount - 2 * totalFiles))); } - String readFooterMetrics = ""; - if (!readFooterMetricsList.isEmpty()) { - readFooterMetrics = getFooterMetrics(readFooterMetricsList); + + @Override + public String toString() { + StringBuilder readFooterMetric = new StringBuilder(); + appendMetrics(readFooterMetric, PARQUET); + appendMetrics(readFooterMetric, NON_PARQUET); + return readFooterMetric.toString(); } - return readFooterMetrics; - } } - diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java index 6512cf647503a..b94cf59bb88d5 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java @@ -18,28 +18,32 @@ protected void setIOStatistics(final IOStatisticsStore ioStatistics) { this.ioStatistics = ioStatistics; } - public long incCounter(String name) { - return incCounter(name, 1); + public void incCounter(String name) { + incCounter(name, 1); } - public long incCounter(String name, long value) { - return ioStatistics.incrementCounter(name, value); + public void incCounter(String name, long value) { + ioStatistics.incrementCounter(name, value); } public Long lookupCounterValue(String name) { - return ioStatistics.counters().get(name); + return ioStatistics.counters().getOrDefault(name, 0L); } public void setCounterValue(String name, long value) { ioStatistics.setCounter(name, value); } + public Long lookupGaugeValue(String name) { + return ioStatistics.gauges().getOrDefault(name, 0L); + } + + public void updateGauge(String name, long value) { + ioStatistics.setGauge(name, lookupGaugeValue(name) + value); + } + @Override public String toString() { - final StringBuilder sb = new StringBuilder( - "AbstractAbfsStatisticsStore{"); - sb.append(ioStatistics); - sb.append('}'); - return sb.toString(); + return "AbstractAbfsStatisticsStore{" + ioStatistics + '}'; } } diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java index 0071b90771c49..35c896c4d7007 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java @@ -26,6 +26,10 @@ import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.ONE_KB; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.ONE_MB; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.DEFAULT_READ_BUFFER_SIZE; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.NON_PARQUET; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.PARQUET; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.*; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; @@ -165,7 +169,7 @@ public void testReadFooterMetrics() throws Exception { // Get non-Parquet metrics and assert metrics equality. AbfsReadFooterMetrics nonParquetMetrics = getNonParquetMetrics(); - String metrics = nonParquetMetrics.getReadFooterMetrics(nonParquetMetrics); + String metrics = nonParquetMetrics.toString(); assertMetricsEquality(fs, metrics); // Close the AzureBlobFileSystem. @@ -177,11 +181,12 @@ public void testReadFooterMetrics() throws Exception { */ private AbfsReadFooterMetrics getNonParquetMetrics() { AbfsReadFooterMetrics nonParquetMetrics = new AbfsReadFooterMetrics(); - nonParquetMetrics.setIsParquetFile(false); - nonParquetMetrics.setSizeReadByFirstRead("16384.000_16384.000"); - nonParquetMetrics.setOffsetDiffBetweenFirstAndSecondRead("1.000_16384.000"); - nonParquetMetrics.setAvgFileLength(Double.parseDouble("32768.000")); - nonParquetMetrics.setAvgReadLenRequested(Double.parseDouble("16384.000")); + nonParquetMetrics.incrementMetricValue(NON_PARQUET, READ_COUNT, 10);; + nonParquetMetrics.updateMetric(NON_PARQUET, FILE_LENGTH, 32768); + nonParquetMetrics.updateMetric(NON_PARQUET, READ_LEN_REQUESTED, 16384); + nonParquetMetrics.updateMetric(NON_PARQUET, SIZE_READ_BY_FIRST_READ, 16384); + nonParquetMetrics.updateMetric(NON_PARQUET, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ, 1); + nonParquetMetrics.incrementMetricValue(NON_PARQUET, TOTAL_FILES); return nonParquetMetrics; } @@ -190,11 +195,12 @@ private AbfsReadFooterMetrics getNonParquetMetrics() { */ private AbfsReadFooterMetrics getParquetMetrics() { AbfsReadFooterMetrics parquetMetrics = new AbfsReadFooterMetrics(); - parquetMetrics.setIsParquetFile(true); - parquetMetrics.setSizeReadByFirstRead("1024.000"); - parquetMetrics.setOffsetDiffBetweenFirstAndSecondRead("4096.000"); - parquetMetrics.setAvgFileLength(Double.parseDouble("8388608.000")); - parquetMetrics.setAvgReadLenRequested(0.000); + parquetMetrics.incrementMetricValue(PARQUET, READ_COUNT, 10);; + parquetMetrics.updateMetric(PARQUET, FILE_LENGTH, 8388608); + parquetMetrics.updateMetric(PARQUET, READ_LEN_REQUESTED, 8388608); + parquetMetrics.updateMetric(PARQUET, SIZE_READ_BY_FIRST_READ, 1024); + parquetMetrics.updateMetric(PARQUET, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ, 4096); + parquetMetrics.incrementMetricValue(PARQUET, TOTAL_FILES); return parquetMetrics; } @@ -307,8 +313,8 @@ private void testReadWriteAndSeek(int fileSize, int bufferSize, Integer seek1, I AbfsReadFooterMetrics nonParquetMetrics = getNonParquetMetrics(); // Concatenate and assert the metrics equality. - String metrics = parquetMetrics.getReadFooterMetrics(parquetMetrics); - metrics += nonParquetMetrics.getReadFooterMetrics(nonParquetMetrics); + String metrics = parquetMetrics.toString(); + metrics += nonParquetMetrics.toString(); assertMetricsEquality(fs, metrics); // Close the AzureBlobFileSystem instance. @@ -375,7 +381,7 @@ public void testMetricWithIdlePeriod() throws Exception { // Get and assert the footer metrics for non-Parquet scenarios. AbfsReadFooterMetrics nonParquetMetrics = getNonParquetMetrics(); - String metrics = nonParquetMetrics.getReadFooterMetrics(nonParquetMetrics); + String metrics = nonParquetMetrics.toString(); assertMetricsEquality(fs, metrics); // Introduce an additional idle period by sleeping. diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java new file mode 100644 index 0000000000000..6c36122b606e4 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java @@ -0,0 +1,21 @@ +package org.apache.hadoop.fs.azurebfs.services; + +import org.assertj.core.api.Assertions; +import org.junit.Test; + +public class TestAbfsReadFooterMetrics { + @Test + public void testReadFooterMetrics() throws Exception { + AbfsReadFooterMetrics metrics = new AbfsReadFooterMetrics(); + metrics.checkMetricUpdate("Test", 1000, 4000, 20); + metrics.checkMetricUpdate("Test", 1000, 4000, 20); + metrics.checkMetricUpdate("Test", 1000, 4000, 20); + metrics.checkMetricUpdate("Test", 1000, 4000, 20); + metrics.checkMetricUpdate("Test1", 1000, 1998, 20); + metrics.checkMetricUpdate("Test1", 988, 1998, 20); + metrics.checkMetricUpdate("Test1", 988, 1998, 20); + Assertions.assertThat(metrics.toString()) + .describedAs("Unexpected thread 'abfs-timer-client' found") + .isEqualTo("$NON_PARQUET:$FR=1000.000_2979.000$SR=994.000_0.000$FL=2999.000$RL=2325.333"); + } +} From dabc17195177fe85e5c65ba8881157a0a93829c6 Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Thu, 17 Oct 2024 01:58:53 -0700 Subject: [PATCH 07/32] Added license to newly added file --- .../azurebfs/constants/MetricsConstants.java | 18 ++++++++++++++++++ .../azurebfs/enums/AbfsBackoffMetricsEnum.java | 18 ++++++++++++++++++ .../enums/AbfsReadFooterMetricsEnum.java | 18 ++++++++++++++++++ .../AbstractAbfsStatisticsSource.java | 18 ++++++++++++++++++ .../services/TestAbfsReadFooterMetrics.java | 18 ++++++++++++++++++ 5 files changed, 90 insertions(+) diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java index caf3623b3451d..4934725f10149 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java @@ -1,3 +1,21 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.apache.hadoop.fs.azurebfs.constants; import java.util.Arrays; diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java index 85f1fe1be3e0e..273d5f50b0c77 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java @@ -1,3 +1,21 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.apache.hadoop.fs.azurebfs.enums; import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.BASE; diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java index f52636d508835..1b7bb917506ef 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java @@ -1,3 +1,21 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.apache.hadoop.fs.azurebfs.enums; import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.COUNTER; diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java index b94cf59bb88d5..f4be91a2303cc 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java @@ -1,3 +1,21 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.apache.hadoop.fs.azurebfs.statistics; import org.apache.hadoop.fs.statistics.IOStatisticsSource; diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java index 6c36122b606e4..42dd394bc580e 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java @@ -1,3 +1,21 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.apache.hadoop.fs.azurebfs.services; import org.assertj.core.api.Assertions; From c24f0eb5e34523390be95ef3532015543f2e73cb Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Thu, 17 Oct 2024 02:31:41 -0700 Subject: [PATCH 08/32] Revert AbfsClient changes --- .../fs/azurebfs/services/AbfsClient.java | 1535 ++++++----------- .../azurebfs/ITestAbfsReadFooterMetrics.java | 6 +- 2 files changed, 572 insertions(+), 969 deletions(-) diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClient.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClient.java index a2d65c145b625..2093e26d8a47a 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClient.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClient.java @@ -31,17 +31,18 @@ import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Base64; +import java.util.Hashtable; import java.util.List; import java.util.Locale; import java.util.Timer; import java.util.TimerTask; -import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.hadoop.classification.VisibleForTesting; +import org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants; import org.apache.hadoop.fs.azurebfs.constants.HttpOperationType; import org.apache.hadoop.fs.azurebfs.constants.FSOperationType; import org.apache.hadoop.fs.azurebfs.contracts.exceptions.AbfsInvalidChecksumException; @@ -67,9 +68,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants; +import org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.ApiVersion; import org.apache.hadoop.fs.azurebfs.constants.HttpHeaderConfigurations; -import org.apache.hadoop.fs.azurebfs.constants.HttpQueryParams; import org.apache.hadoop.fs.azurebfs.contracts.exceptions.AbfsRestOperationException; import org.apache.hadoop.fs.azurebfs.contracts.exceptions.AzureBlobFileSystemException; import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidUriException; @@ -83,27 +83,55 @@ import org.apache.hadoop.security.ssl.DelegatingSSLSocketFactory; import org.apache.hadoop.util.concurrent.HadoopExecutors; -import static org.apache.commons.lang3.StringUtils.isEmpty; import static org.apache.commons.lang3.StringUtils.isNotEmpty; import static org.apache.hadoop.fs.azurebfs.AbfsStatistic.RENAME_PATH_ATTEMPTS; import static org.apache.hadoop.fs.azurebfs.AzureBlobFileSystemStore.extractEtagHeader; -import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.*; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.APN_VERSION; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.CLIENT_VERSION; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.DEFAULT_TIMEOUT; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.EMPTY_STRING; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.FILESYSTEM; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.FORWARD_SLASH; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.FORWARD_SLASH_ENCODE; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.HTTP_METHOD_DELETE; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.HTTP_METHOD_HEAD; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.HTTP_METHOD_PUT; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.HUNDRED_CONTINUE; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.JAVA_VENDOR; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.JAVA_VERSION; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.MD5; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.OS_ARCH; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.OS_NAME; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.OS_VERSION; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.PLUS; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.PLUS_ENCODE; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.SEMICOLON; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.SINGLE_WHITE_SPACE; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.UTF_8; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.DEFAULT_DELETE_CONSIDERED_IDEMPOTENT; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.ONE_MB; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.SERVER_SIDE_ENCRYPTION_ALGORITHM; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemUriSchemes.HTTPS_SCHEME; -import static org.apache.hadoop.fs.azurebfs.constants.HttpHeaderConfigurations.*; -import static org.apache.hadoop.fs.azurebfs.constants.HttpQueryParams.*; -import static org.apache.hadoop.fs.azurebfs.contracts.services.AzureServiceErrorCode.RENAME_DESTINATION_PARENT_PATH_NOT_FOUND; -import static org.apache.hadoop.fs.azurebfs.contracts.services.AzureServiceErrorCode.SOURCE_PATH_NOT_FOUND; +import static org.apache.hadoop.fs.azurebfs.constants.HttpHeaderConfigurations.ACCEPT_CHARSET; +import static org.apache.hadoop.fs.azurebfs.constants.HttpHeaderConfigurations.CONTENT_MD5; +import static org.apache.hadoop.fs.azurebfs.constants.HttpHeaderConfigurations.CONTENT_TYPE; +import static org.apache.hadoop.fs.azurebfs.constants.HttpHeaderConfigurations.USER_AGENT; +import static org.apache.hadoop.fs.azurebfs.constants.HttpHeaderConfigurations.X_MS_ENCRYPTION_ALGORITHM; +import static org.apache.hadoop.fs.azurebfs.constants.HttpHeaderConfigurations.X_MS_ENCRYPTION_CONTEXT; +import static org.apache.hadoop.fs.azurebfs.constants.HttpHeaderConfigurations.X_MS_ENCRYPTION_KEY; +import static org.apache.hadoop.fs.azurebfs.constants.HttpHeaderConfigurations.X_MS_ENCRYPTION_KEY_SHA256; +import static org.apache.hadoop.fs.azurebfs.constants.HttpHeaderConfigurations.X_MS_VERSION; +import static org.apache.hadoop.fs.azurebfs.constants.HttpQueryParams.QUERY_PARAM_RESOURCE; +import static org.apache.hadoop.fs.azurebfs.constants.HttpQueryParams.QUERY_PARAM_TIMEOUT; import static org.apache.hadoop.fs.azurebfs.services.RetryReasonConstants.CONNECTION_TIMEOUT_ABBREVIATION; /** * AbfsClient. */ -public class AbfsClient implements Closeable { +public abstract class AbfsClient implements Closeable { public static final Logger LOG = LoggerFactory.getLogger(AbfsClient.class); public static final String HUNDRED_CONTINUE_USER_AGENT = SINGLE_WHITE_SPACE + HUNDRED_CONTINUE + SEMICOLON; + public static final String ABFS_CLIENT_TIMER_THREAD_NAME = "abfs-timer-client"; private final URL baseUrl; private final SharedKeyCredentials sharedKeyCredentials; @@ -122,7 +150,7 @@ public class AbfsClient implements Closeable { private AccessTokenProvider tokenProvider; private SASTokenProvider sasTokenProvider; private final AbfsCounters abfsCounters; - private final Timer timer; + private Timer timer; private final String abfsMetricUrl; private boolean isMetricCollectionEnabled = false; private final MetricFormat metricFormat; @@ -147,13 +175,13 @@ public class AbfsClient implements Closeable { /** * logging the rename failure if metadata is in an incomplete state. */ - private static final LogExactlyOnce ABFS_METADATA_INCOMPLETE_RENAME_FAILURE = new LogExactlyOnce(LOG); + protected static final LogExactlyOnce ABFS_METADATA_INCOMPLETE_RENAME_FAILURE = new LogExactlyOnce(LOG); private AbfsClient(final URL baseUrl, - final SharedKeyCredentials sharedKeyCredentials, - final AbfsConfiguration abfsConfiguration, - final EncryptionContextProvider encryptionContextProvider, - final AbfsClientContext abfsClientContext) throws IOException { + final SharedKeyCredentials sharedKeyCredentials, + final AbfsConfiguration abfsConfiguration, + final EncryptionContextProvider encryptionContextProvider, + final AbfsClientContext abfsClientContext) throws IOException { this.baseUrl = baseUrl; this.sharedKeyCredentials = sharedKeyCredentials; String baseUrlString = baseUrl.toString(); @@ -172,9 +200,9 @@ private AbfsClient(final URL baseUrl, encryptionType = EncryptionType.ENCRYPTION_CONTEXT; } else if (abfsConfiguration.getEncodedClientProvidedEncryptionKey() != null) { clientProvidedEncryptionKey = - abfsConfiguration.getEncodedClientProvidedEncryptionKey(); + abfsConfiguration.getEncodedClientProvidedEncryptionKey(); this.clientProvidedEncryptionKeySHA = - abfsConfiguration.getEncodedClientProvidedEncryptionKeySHA(); + abfsConfiguration.getEncodedClientProvidedEncryptionKeySHA(); encryptionType = EncryptionType.GLOBAL_KEY; } @@ -189,17 +217,17 @@ private AbfsClient(final URL baseUrl, } catch (IOException e) { // Suppress exception, failure to init DelegatingSSLSocketFactory would have only performance impact. LOG.trace("NonCritFailure: DelegatingSSLSocketFactory Init failed : " - + "{}", e.getMessage()); + + "{}", e.getMessage()); } } if (abfsConfiguration.getPreferredHttpOperationType() - == HttpOperationType.APACHE_HTTP_CLIENT) { + == HttpOperationType.APACHE_HTTP_CLIENT) { keepAliveCache = new KeepAliveCache(abfsConfiguration); abfsApacheHttpClient = new AbfsApacheHttpClient( - DelegatingSSLSocketFactory.getDefaultFactory(), - abfsConfiguration.getHttpReadTimeout(), - keepAliveCache); + DelegatingSSLSocketFactory.getDefaultFactory(), + abfsConfiguration.getHttpReadTimeout(), + keepAliveCache); } this.userAgent = initializeUserAgent(abfsConfiguration, sslProviderName); @@ -207,9 +235,9 @@ private AbfsClient(final URL baseUrl, this.abfsCounters = abfsClientContext.getAbfsCounters(); ThreadFactory tf = - new ThreadFactoryBuilder().setNameFormat("AbfsClient Lease Ops").setDaemon(true).build(); + new ThreadFactoryBuilder().setNameFormat("AbfsClient Lease Ops").setDaemon(true).build(); this.executorService = MoreExecutors.listeningDecorator( - HadoopExecutors.newScheduledThreadPool(this.abfsConfiguration.getNumLeaseThreads(), tf)); + HadoopExecutors.newScheduledThreadPool(this.abfsConfiguration.getNumLeaseThreads(), tf)); this.metricFormat = abfsConfiguration.getMetricFormat(); this.isMetricCollectionStopped = new AtomicBoolean(false); this.metricAnalysisPeriod = abfsConfiguration.getMetricAnalysisTimeout(); @@ -231,12 +259,12 @@ private AbfsClient(final URL baseUrl, throw new IOException("Exception while initializing metric credentials " + e); } } - this.timer = new Timer( - "abfs-timer-client", true); if (isMetricCollectionEnabled) { + this.timer = new Timer( + ABFS_CLIENT_TIMER_THREAD_NAME, true); timer.schedule(new TimerTaskImpl(), - metricIdlePeriod, - metricIdlePeriod); + metricIdlePeriod, + metricIdlePeriod); } this.abfsMetricUrl = abfsConfiguration.getMetricUri(); } @@ -246,28 +274,28 @@ public AbfsClient(final URL baseUrl, final SharedKeyCredentials sharedKeyCredent final AccessTokenProvider tokenProvider, final EncryptionContextProvider encryptionContextProvider, final AbfsClientContext abfsClientContext) - throws IOException { + throws IOException { this(baseUrl, sharedKeyCredentials, abfsConfiguration, - encryptionContextProvider, abfsClientContext); + encryptionContextProvider, abfsClientContext); this.tokenProvider = tokenProvider; } public AbfsClient(final URL baseUrl, final SharedKeyCredentials sharedKeyCredentials, final AbfsConfiguration abfsConfiguration, final SASTokenProvider sasTokenProvider, - final EncryptionContextProvider encryptionContextProvider, + final EncryptionContextProvider encryptionContextProvider, final AbfsClientContext abfsClientContext) - throws IOException { + throws IOException { this(baseUrl, sharedKeyCredentials, abfsConfiguration, - encryptionContextProvider, abfsClientContext); + encryptionContextProvider, abfsClientContext); this.sasTokenProvider = sasTokenProvider; } @Override public void close() throws IOException { - if (runningTimerTask != null) { + if (isMetricCollectionEnabled && runningTimerTask != null) { runningTimerTask.cancel(); - timer.purge(); + timer.cancel(); } if (keepAliveCache != null) { keepAliveCache.close(); @@ -277,7 +305,7 @@ public void close() throws IOException { } if (tokenProvider instanceof Closeable) { IOUtils.cleanupWithLogger(LOG, - (Closeable) tokenProvider); + (Closeable) tokenProvider); } HadoopExecutors.shutdown(executorService, LOG, 0, TimeUnit.SECONDS); } @@ -305,9 +333,9 @@ StaticRetryPolicy getStaticRetryPolicy() { */ public AbfsRetryPolicy getRetryPolicy(final String failureReason) { return CONNECTION_TIMEOUT_ABBREVIATION.equals(failureReason) - && getAbfsConfiguration().getStaticRetryForConnectionTimeoutEnabled() - ? getStaticRetryPolicy() - : getExponentialRetryPolicy(); + && getAbfsConfiguration().getStaticRetryForConnectionTimeoutEnabled() + ? getStaticRetryPolicy() + : getExponentialRetryPolicy(); } SharedKeyCredentials getSharedKeyCredentials() { @@ -335,22 +363,25 @@ AbfsThrottlingIntercept getIntercept() { * @return default request headers */ @VisibleForTesting - protected List createDefaultHeaders() { - return createDefaultHeaders(this.xMsVersion); - } + protected abstract List createDefaultHeaders(); /** * Create request headers for Rest Operation using the specified API version. - * @param xMsVersion + * @param xMsVersion Azure services API version to be used. * @return default request headers */ - private List createDefaultHeaders(ApiVersion xMsVersion) { + @VisibleForTesting + public abstract List createDefaultHeaders(ApiVersion xMsVersion); + + /** + * Create request headers common to both service endpoints. + * @param xMsVersion azure services API version to be used. + * @return common request headers + */ + protected List createCommonHeaders(ApiVersion xMsVersion) { final List requestHeaders = new ArrayList(); requestHeaders.add(new AbfsHttpHeader(X_MS_VERSION, xMsVersion.toString())); - requestHeaders.add(new AbfsHttpHeader(ACCEPT, APPLICATION_JSON - + COMMA + SINGLE_WHITE_SPACE + APPLICATION_OCTET_STREAM)); - requestHeaders.add(new AbfsHttpHeader(ACCEPT_CHARSET, - UTF_8)); + requestHeaders.add(new AbfsHttpHeader(ACCEPT_CHARSET, UTF_8)); requestHeaders.add(new AbfsHttpHeader(CONTENT_TYPE, EMPTY_STRING)); requestHeaders.add(new AbfsHttpHeader(USER_AGENT, userAgent)); return requestHeaders; @@ -372,139 +403,106 @@ private List createDefaultHeaders(ApiVersion xMsVersion) { *

  • getPathStatus for fs.setXAttr and fs.getXAttr
  • *
  • read
  • * + * @param path path of the file / directory to be created / overwritten. + * @param requestHeaders list of headers to be added to the request. + * @param isCreateFileRequest defines if file or directory has to be created / overwritten. + * @param contextEncryptionAdapter object that contains the encryptionContext and + * encryptionKey created from the developer provided implementation of {@link EncryptionContextProvider} + * @param tracingContext to trace service calls. + * @throws AzureBlobFileSystemException if namespace is not enabled. */ - private void addEncryptionKeyRequestHeaders(String path, - List requestHeaders, boolean isCreateFileRequest, - ContextEncryptionAdapter contextEncryptionAdapter, TracingContext tracingContext) - throws AzureBlobFileSystemException { + protected void addEncryptionKeyRequestHeaders(String path, + List requestHeaders, boolean isCreateFileRequest, + ContextEncryptionAdapter contextEncryptionAdapter, TracingContext tracingContext) + throws AzureBlobFileSystemException { String encodedKey, encodedKeySHA256; switch (encryptionType) { - case GLOBAL_KEY: - encodedKey = clientProvidedEncryptionKey; - encodedKeySHA256 = clientProvidedEncryptionKeySHA; - break; - - case ENCRYPTION_CONTEXT: - if (isCreateFileRequest) { - // get new context for create file request - requestHeaders.add(new AbfsHttpHeader(X_MS_ENCRYPTION_CONTEXT, - contextEncryptionAdapter.getEncodedContext())); - } - // else use cached encryption keys from input/output streams - encodedKey = contextEncryptionAdapter.getEncodedKey(); - encodedKeySHA256 = contextEncryptionAdapter.getEncodedKeySHA(); - break; + case GLOBAL_KEY: + encodedKey = clientProvidedEncryptionKey; + encodedKeySHA256 = clientProvidedEncryptionKeySHA; + break; + + case ENCRYPTION_CONTEXT: + if (isCreateFileRequest) { + // get new context for create file request + requestHeaders.add(new AbfsHttpHeader(X_MS_ENCRYPTION_CONTEXT, + contextEncryptionAdapter.getEncodedContext())); + } + // else use cached encryption keys from input/output streams + encodedKey = contextEncryptionAdapter.getEncodedKey(); + encodedKeySHA256 = contextEncryptionAdapter.getEncodedKeySHA(); + break; - default: return; // no client-provided encryption keys + default: return; // no client-provided encryption keys } requestHeaders.add(new AbfsHttpHeader(X_MS_ENCRYPTION_KEY, encodedKey)); requestHeaders.add( - new AbfsHttpHeader(X_MS_ENCRYPTION_KEY_SHA256, encodedKeySHA256)); + new AbfsHttpHeader(X_MS_ENCRYPTION_KEY_SHA256, encodedKeySHA256)); requestHeaders.add(new AbfsHttpHeader(X_MS_ENCRYPTION_ALGORITHM, - SERVER_SIDE_ENCRYPTION_ALGORITHM)); + SERVER_SIDE_ENCRYPTION_ALGORITHM)); } - AbfsUriQueryBuilder createDefaultUriQueryBuilder() { + /** + * Creates a AbfsUriQueryBuilder with default query parameter timeout. + * @return default AbfsUriQueryBuilder. + */ + protected AbfsUriQueryBuilder createDefaultUriQueryBuilder() { final AbfsUriQueryBuilder abfsUriQueryBuilder = new AbfsUriQueryBuilder(); abfsUriQueryBuilder.addQuery(QUERY_PARAM_TIMEOUT, DEFAULT_TIMEOUT); return abfsUriQueryBuilder; } - public AbfsRestOperation createFilesystem(TracingContext tracingContext) - throws AzureBlobFileSystemException { - final List requestHeaders = createDefaultHeaders(); - - final AbfsUriQueryBuilder abfsUriQueryBuilder = new AbfsUriQueryBuilder(); - abfsUriQueryBuilder.addQuery(QUERY_PARAM_RESOURCE, FILESYSTEM); - - final URL url = createRequestUrl(abfsUriQueryBuilder.toString()); - final AbfsRestOperation op = getAbfsRestOperation( - AbfsRestOperationType.CreateFileSystem, - HTTP_METHOD_PUT, url, requestHeaders); - op.execute(tracingContext); - return op; - } - - public AbfsRestOperation setFilesystemProperties(final String properties, - TracingContext tracingContext) throws AzureBlobFileSystemException { - final List requestHeaders = createDefaultHeaders(); - // JDK7 does not support PATCH, so to work around the issue we will use - // PUT and specify the real method in the X-Http-Method-Override header. - requestHeaders.add(new AbfsHttpHeader(X_HTTP_METHOD_OVERRIDE, - HTTP_METHOD_PATCH)); - - requestHeaders.add(new AbfsHttpHeader(X_MS_PROPERTIES, - properties)); - - final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); - abfsUriQueryBuilder.addQuery(QUERY_PARAM_RESOURCE, FILESYSTEM); - - final URL url = createRequestUrl(abfsUriQueryBuilder.toString()); - final AbfsRestOperation op = getAbfsRestOperation( - AbfsRestOperationType.SetFileSystemProperties, - HTTP_METHOD_PUT, - url, - requestHeaders); - op.execute(tracingContext); - return op; - } - - public AbfsRestOperation listPath(final String relativePath, final boolean recursive, final int listMaxResults, - final String continuation, TracingContext tracingContext) - throws IOException { - final List requestHeaders = createDefaultHeaders(); - - final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); - abfsUriQueryBuilder.addQuery(QUERY_PARAM_RESOURCE, FILESYSTEM); - abfsUriQueryBuilder.addQuery(QUERY_PARAM_DIRECTORY, getDirectoryQueryParameter(relativePath)); - abfsUriQueryBuilder.addQuery(QUERY_PARAM_RECURSIVE, String.valueOf(recursive)); - abfsUriQueryBuilder.addQuery(QUERY_PARAM_CONTINUATION, continuation); - abfsUriQueryBuilder.addQuery(QUERY_PARAM_MAXRESULTS, String.valueOf(listMaxResults)); - abfsUriQueryBuilder.addQuery(HttpQueryParams.QUERY_PARAM_UPN, String.valueOf(abfsConfiguration.isUpnUsed())); - appendSASTokenToQuery(relativePath, SASTokenProvider.LIST_OPERATION, abfsUriQueryBuilder); - - final URL url = createRequestUrl(abfsUriQueryBuilder.toString()); - final AbfsRestOperation op = getAbfsRestOperation( - AbfsRestOperationType.ListPaths, - HTTP_METHOD_GET, - url, - requestHeaders); - op.execute(tracingContext); - return op; - } - - public AbfsRestOperation getFilesystemProperties(TracingContext tracingContext) throws AzureBlobFileSystemException { - final List requestHeaders = createDefaultHeaders(); - - final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); - abfsUriQueryBuilder.addQuery(QUERY_PARAM_RESOURCE, FILESYSTEM); + /** + * Create a new filesystem using Azure REST API Service. + * @param tracingContext for tracing the server calls. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation fails. + */ + public abstract AbfsRestOperation createFilesystem(TracingContext tracingContext) + throws AzureBlobFileSystemException; - final URL url = createRequestUrl(abfsUriQueryBuilder.toString()); - final AbfsRestOperation op = getAbfsRestOperation( - AbfsRestOperationType.GetFileSystemProperties, - HTTP_METHOD_HEAD, - url, - requestHeaders); - op.execute(tracingContext); - return op; - } + /** + * Sets user-defined metadata on filesystem. + * @param properties list of metadata key-value pairs. + * @param tracingContext for tracing the server calls. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation fails. + */ + public abstract AbfsRestOperation setFilesystemProperties(Hashtable properties, + TracingContext tracingContext) throws AzureBlobFileSystemException; - public AbfsRestOperation deleteFilesystem(TracingContext tracingContext) throws AzureBlobFileSystemException { - final List requestHeaders = createDefaultHeaders(); + /** + * List paths and their properties in the current filesystem. + * @param relativePath to return only blobs within this directory. + * @param recursive to return all blobs in the path, including those in subdirectories. + * @param listMaxResults maximum number of blobs to return. + * @param continuation marker to specify the continuation token. + * @param tracingContext for tracing the server calls. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation or response parsing fails. + */ + public abstract AbfsRestOperation listPath(String relativePath, boolean recursive, + int listMaxResults, String continuation, TracingContext tracingContext) + throws IOException; - final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); - abfsUriQueryBuilder.addQuery(QUERY_PARAM_RESOURCE, FILESYSTEM); + /** + * Retrieves user-defined metadata on filesystem. + * @param tracingContext for tracing the server calls. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation fails. + * */ + public abstract AbfsRestOperation getFilesystemProperties(TracingContext tracingContext) + throws AzureBlobFileSystemException; - final URL url = createRequestUrl(abfsUriQueryBuilder.toString()); - final AbfsRestOperation op = getAbfsRestOperation( - AbfsRestOperationType.DeleteFileSystem, - HTTP_METHOD_DELETE, - url, - requestHeaders); - op.execute(tracingContext); - return op; - } + /** + * Deletes the filesystem using Azure REST API Service. + * @param tracingContext for tracing the server calls. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation fails. + */ + public abstract AbfsRestOperation deleteFilesystem(TracingContext tracingContext) + throws AzureBlobFileSystemException; /** * Method for calling createPath API to the backend. Method can be called from: @@ -533,150 +531,57 @@ public AbfsRestOperation deleteFilesystem(TracingContext tracingContext) throws * @throws AzureBlobFileSystemException throws back the exception it receives from the * {@link AbfsRestOperation#execute(TracingContext)} method call. */ - public AbfsRestOperation createPath(final String path, - final boolean isFile, - final boolean overwrite, - final Permissions permissions, - final boolean isAppendBlob, - final String eTag, - final ContextEncryptionAdapter contextEncryptionAdapter, - final TracingContext tracingContext) - throws AzureBlobFileSystemException { - final List requestHeaders = createDefaultHeaders(); - if (isFile) { - addEncryptionKeyRequestHeaders(path, requestHeaders, true, - contextEncryptionAdapter, tracingContext); - } - if (!overwrite) { - requestHeaders.add(new AbfsHttpHeader(IF_NONE_MATCH, AbfsHttpConstants.STAR)); - } - - if (permissions.hasPermission()) { - requestHeaders.add( - new AbfsHttpHeader(HttpHeaderConfigurations.X_MS_PERMISSIONS, - permissions.getPermission())); - } - - if (permissions.hasUmask()) { - requestHeaders.add(new AbfsHttpHeader(HttpHeaderConfigurations.X_MS_UMASK, - permissions.getUmask())); - } + public abstract AbfsRestOperation createPath(String path, + boolean isFile, + boolean overwrite, + Permissions permissions, + boolean isAppendBlob, + String eTag, + ContextEncryptionAdapter contextEncryptionAdapter, + TracingContext tracingContext) throws AzureBlobFileSystemException; - if (eTag != null && !eTag.isEmpty()) { - requestHeaders.add(new AbfsHttpHeader(HttpHeaderConfigurations.IF_MATCH, eTag)); - } - - final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); - abfsUriQueryBuilder.addQuery(QUERY_PARAM_RESOURCE, isFile ? FILE : DIRECTORY); - if (isAppendBlob) { - abfsUriQueryBuilder.addQuery(QUERY_PARAM_BLOBTYPE, APPEND_BLOB_TYPE); - } - - String operation = isFile - ? SASTokenProvider.CREATE_FILE_OPERATION - : SASTokenProvider.CREATE_DIRECTORY_OPERATION; - appendSASTokenToQuery(path, operation, abfsUriQueryBuilder); - - final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); - final AbfsRestOperation op = getAbfsRestOperation( - AbfsRestOperationType.CreatePath, - HTTP_METHOD_PUT, - url, - requestHeaders); - try { - op.execute(tracingContext); - } catch (AzureBlobFileSystemException ex) { - // If we have no HTTP response, throw the original exception. - if (!op.hasResult()) { - throw ex; - } - if (!isFile && op.getResult().getStatusCode() == HttpURLConnection.HTTP_CONFLICT) { - String existingResource = - op.getResult().getResponseHeader(X_MS_EXISTING_RESOURCE_TYPE); - if (existingResource != null && existingResource.equals(DIRECTORY)) { - return op; //don't throw ex on mkdirs for existing directory - } - } - throw ex; - } - return op; - } - - public AbfsRestOperation acquireLease(final String path, int duration, TracingContext tracingContext) throws AzureBlobFileSystemException { - final List requestHeaders = createDefaultHeaders(); - - requestHeaders.add(new AbfsHttpHeader(X_MS_LEASE_ACTION, ACQUIRE_LEASE_ACTION)); - requestHeaders.add(new AbfsHttpHeader(X_MS_LEASE_DURATION, Integer.toString(duration))); - requestHeaders.add(new AbfsHttpHeader(X_MS_PROPOSED_LEASE_ID, UUID.randomUUID().toString())); - - final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); - - final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); - final AbfsRestOperation op = getAbfsRestOperation( - AbfsRestOperationType.LeasePath, - HTTP_METHOD_POST, - url, - requestHeaders); - op.execute(tracingContext); - return op; - } - - public AbfsRestOperation renewLease(final String path, final String leaseId, - TracingContext tracingContext) throws AzureBlobFileSystemException { - final List requestHeaders = createDefaultHeaders(); - - requestHeaders.add(new AbfsHttpHeader(X_MS_LEASE_ACTION, RENEW_LEASE_ACTION)); - requestHeaders.add(new AbfsHttpHeader(X_MS_LEASE_ID, leaseId)); - - final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); - - final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); - final AbfsRestOperation op = getAbfsRestOperation( - AbfsRestOperationType.LeasePath, - HTTP_METHOD_POST, - url, - requestHeaders); - op.execute(tracingContext); - return op; - } - - public AbfsRestOperation releaseLease(final String path, - final String leaseId, TracingContext tracingContext) throws AzureBlobFileSystemException { - final List requestHeaders = createDefaultHeaders(); - - requestHeaders.add(new AbfsHttpHeader(X_MS_LEASE_ACTION, RELEASE_LEASE_ACTION)); - requestHeaders.add(new AbfsHttpHeader(X_MS_LEASE_ID, leaseId)); - - final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); - - final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); - final AbfsRestOperation op = getAbfsRestOperation( - AbfsRestOperationType.LeasePath, - HTTP_METHOD_POST, - url, - requestHeaders); - op.execute(tracingContext); - return op; - } - - public AbfsRestOperation breakLease(final String path, - TracingContext tracingContext) throws AzureBlobFileSystemException { - final List requestHeaders = createDefaultHeaders(); + /** + * Acquire lease on specified path. + * @param path on which lease has to be acquired. + * @param duration for which lease has to be acquired. + * @param tracingContext for tracing the server calls. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation fails. + */ + public abstract AbfsRestOperation acquireLease(String path, int duration, + TracingContext tracingContext) throws AzureBlobFileSystemException; - requestHeaders.add(new AbfsHttpHeader(X_MS_LEASE_ACTION, BREAK_LEASE_ACTION)); - requestHeaders.add(new AbfsHttpHeader(X_MS_LEASE_BREAK_PERIOD, DEFAULT_LEASE_BREAK_PERIOD)); + /** + * Renew lease on specified path. + * @param path on which lease has to be renewed. + * @param leaseId of the lease to be renewed. + * @param tracingContext for tracing the server calls. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation fails. + */ + public abstract AbfsRestOperation renewLease(String path, String leaseId, + TracingContext tracingContext) throws AzureBlobFileSystemException; - final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); + /** + * Release lease on specified path. + * @param path on which lease has to be released. + * @param leaseId of the lease to be released. + * @param tracingContext for tracing the server calls. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation fails. + */ + public abstract AbfsRestOperation releaseLease(String path, String leaseId, + TracingContext tracingContext) throws AzureBlobFileSystemException; - final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); - final AbfsRestOperation op = getAbfsRestOperation( - AbfsRestOperationType.LeasePath, - HTTP_METHOD_POST, - url, - requestHeaders); - op.execute(tracingContext); - return op; - } + /** + * Break lease on specified path. + * @param path on which lease has to be broke. + * @param tracingContext for tracing the server calls. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation fails. + */ + public abstract AbfsRestOperation breakLease(String path, + TracingContext tracingContext) throws AzureBlobFileSystemException; /** * Rename a file or directory. @@ -700,127 +605,29 @@ public AbfsRestOperation breakLease(final String path, * AbfsRest operation, rename recovery and incomplete metadata state failure. * @throws AzureBlobFileSystemException failure, excluding any recovery from overload failures. */ - public AbfsClientRenameResult renamePath( - final String source, - final String destination, - final String continuation, - final TracingContext tracingContext, + public abstract AbfsClientRenameResult renamePath( + String source, + String destination, + String continuation, + TracingContext tracingContext, String sourceEtag, boolean isMetadataIncompleteState, boolean isNamespaceEnabled) - throws IOException { - final List requestHeaders = createDefaultHeaders(); - - final boolean hasEtag = !isEmpty(sourceEtag); - - boolean shouldAttemptRecovery = renameResilience && isNamespaceEnabled; - if (!hasEtag && shouldAttemptRecovery) { - // in case eTag is already not supplied to the API - // and rename resilience is expected and it is an HNS enabled account - // fetch the source etag to be used later in recovery - try { - final AbfsRestOperation srcStatusOp = getPathStatus(source, - false, tracingContext, null); - if (srcStatusOp.hasResult()) { - final AbfsHttpOperation result = srcStatusOp.getResult(); - sourceEtag = extractEtagHeader(result); - // and update the directory status. - boolean isDir = checkIsDir(result); - shouldAttemptRecovery = !isDir; - LOG.debug("Retrieved etag of source for rename recovery: {}; isDir={}", sourceEtag, isDir); - } - } catch (AbfsRestOperationException e) { - throw new AbfsRestOperationException(e.getStatusCode(), SOURCE_PATH_NOT_FOUND.getErrorCode(), - e.getMessage(), e); - } - - } - - String encodedRenameSource = urlEncode(FORWARD_SLASH + this.getFileSystem() + source); - if (authType == AuthType.SAS) { - final AbfsUriQueryBuilder srcQueryBuilder = new AbfsUriQueryBuilder(); - appendSASTokenToQuery(source, SASTokenProvider.RENAME_SOURCE_OPERATION, srcQueryBuilder); - encodedRenameSource += srcQueryBuilder.toString(); - } - - LOG.trace("Rename source queryparam added {}", encodedRenameSource); - requestHeaders.add(new AbfsHttpHeader(X_MS_RENAME_SOURCE, encodedRenameSource)); - requestHeaders.add(new AbfsHttpHeader(IF_NONE_MATCH, STAR)); - - final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); - abfsUriQueryBuilder.addQuery(QUERY_PARAM_CONTINUATION, continuation); - appendSASTokenToQuery(destination, SASTokenProvider.RENAME_DESTINATION_OPERATION, abfsUriQueryBuilder); - - final URL url = createRequestUrl(destination, abfsUriQueryBuilder.toString()); - final AbfsRestOperation op = createRenameRestOperation(url, requestHeaders); - try { - incrementAbfsRenamePath(); - op.execute(tracingContext); - // AbfsClientResult contains the AbfsOperation, If recovery happened or - // not, and the incompleteMetaDataState is true or false. - // If we successfully rename a path and isMetadataIncompleteState was - // true, then rename was recovered, else it didn't, this is why - // isMetadataIncompleteState is used for renameRecovery(as the 2nd param). - return new AbfsClientRenameResult(op, isMetadataIncompleteState, isMetadataIncompleteState); - } catch (AzureBlobFileSystemException e) { - // If we have no HTTP response, throw the original exception. - if (!op.hasResult()) { - throw e; - } - - // ref: HADOOP-18242. Rename failure occurring due to a rare case of - // tracking metadata being in incomplete state. - if (op.getResult().getStorageErrorCode() - .equals(RENAME_DESTINATION_PARENT_PATH_NOT_FOUND.getErrorCode()) - && !isMetadataIncompleteState) { - //Logging - ABFS_METADATA_INCOMPLETE_RENAME_FAILURE - .info("Rename Failure attempting to resolve tracking metadata state and retrying."); - // rename recovery should be attempted in this case also - shouldAttemptRecovery = true; - isMetadataIncompleteState = true; - String sourceEtagAfterFailure = sourceEtag; - if (isEmpty(sourceEtagAfterFailure)) { - // Doing a HEAD call resolves the incomplete metadata state and - // then we can retry the rename operation. - AbfsRestOperation sourceStatusOp = getPathStatus(source, false, - tracingContext, null); - isMetadataIncompleteState = true; - // Extract the sourceEtag, using the status Op, and set it - // for future rename recovery. - AbfsHttpOperation sourceStatusResult = sourceStatusOp.getResult(); - sourceEtagAfterFailure = extractEtagHeader(sourceStatusResult); - } - renamePath(source, destination, continuation, tracingContext, - sourceEtagAfterFailure, isMetadataIncompleteState, isNamespaceEnabled); - } - // if we get out of the condition without a successful rename, then - // it isn't metadata incomplete state issue. - isMetadataIncompleteState = false; - - // setting default rename recovery success to false - boolean etagCheckSucceeded = false; - if (shouldAttemptRecovery) { - etagCheckSucceeded = renameIdempotencyCheckOp( - source, - sourceEtag, op, destination, tracingContext); - } - if (!etagCheckSucceeded) { - // idempotency did not return different result - // throw back the exception - throw e; - } - return new AbfsClientRenameResult(op, true, isMetadataIncompleteState); - } - } + throws IOException; - private boolean checkIsDir(AbfsHttpOperation result) { - String resourceType = result.getResponseHeader( - HttpHeaderConfigurations.X_MS_RESOURCE_TYPE); - return resourceType != null - && resourceType.equalsIgnoreCase(AbfsHttpConstants.DIRECTORY); - } + /** + * Checks if the rest operation results indicate if the path is a directory. + * @param result executed rest operation containing response from server. + * @return True if the path is a directory, False otherwise. + */ + protected abstract boolean checkIsDir(AbfsHttpOperation result); + /** + * Creates a rest operation for rename. + * @param url to be used for the operation. + * @param requestHeaders list of headers to be added to the request. + * @return un-executed rest operation. + */ @VisibleForTesting AbfsRestOperation createRenameRestOperation(URL url, List requestHeaders) { AbfsRestOperation op = getAbfsRestOperation( @@ -831,7 +638,11 @@ AbfsRestOperation createRenameRestOperation(URL url, List reques return op; } - private void incrementAbfsRenamePath() { + /** + * Increments AbfsCounters for rename path attempts by 1. + * Will be called each time a rename path operation is attempted. + */ + protected void incrementAbfsRenamePath() { abfsCounters.incrementCounter(RENAME_PATH_ATTEMPTS, 1); } @@ -853,16 +664,16 @@ private void incrementAbfsRenamePath() { * @return true if the file was successfully copied */ public boolean renameIdempotencyCheckOp( - final String source, - final String sourceEtag, - final AbfsRestOperation op, - final String destination, - TracingContext tracingContext) { + final String source, + final String sourceEtag, + final AbfsRestOperation op, + final String destination, + TracingContext tracingContext) { Preconditions.checkArgument(op.hasResult(), "Operations has null HTTP response"); // removing isDir from debug logs as it can be misleading LOG.debug("rename({}, {}) failure {}; retry={} etag {}", - source, destination, op.getResult().getStatusCode(), op.isARetriedRequest(), sourceEtag); + source, destination, op.getResult().getStatusCode(), op.isARetriedRequest(), sourceEtag); if (!(op.isARetriedRequest() && (op.getResult().getStatusCode() == HttpURLConnection.HTTP_NOT_FOUND))) { // only attempt recovery if the failure was a 404 on a retried rename request. @@ -876,7 +687,7 @@ public boolean renameIdempotencyCheckOp( source, destination); try { final AbfsRestOperation destStatusOp = getPathStatus(destination, - false, tracingContext, null); + false, tracingContext, null); final AbfsHttpOperation result = destStatusOp.getResult(); final boolean recovered = result.getStatusCode() == HttpURLConnection.HTTP_OK @@ -893,145 +704,38 @@ public boolean renameIdempotencyCheckOp( } else { LOG.debug("No source etag; unable to probe for the operation's success"); } - return false; - } - - @VisibleForTesting - boolean isSourceDestEtagEqual(String sourceEtag, AbfsHttpOperation result) { - return sourceEtag.equals(extractEtagHeader(result)); + return false; } - public AbfsRestOperation append(final String path, final byte[] buffer, - AppendRequestParameters reqParams, final String cachedSasToken, - ContextEncryptionAdapter contextEncryptionAdapter, TracingContext tracingContext) - throws AzureBlobFileSystemException { - final List requestHeaders = createDefaultHeaders(); - addEncryptionKeyRequestHeaders(path, requestHeaders, false, - contextEncryptionAdapter, tracingContext); - if (reqParams.isExpectHeaderEnabled()) { - requestHeaders.add(new AbfsHttpHeader(EXPECT, HUNDRED_CONTINUE)); - } - // JDK7 does not support PATCH, so to workaround the issue we will use - // PUT and specify the real method in the X-Http-Method-Override header. - requestHeaders.add(new AbfsHttpHeader(X_HTTP_METHOD_OVERRIDE, - HTTP_METHOD_PATCH)); - if (reqParams.getLeaseId() != null) { - requestHeaders.add(new AbfsHttpHeader(X_MS_LEASE_ID, reqParams.getLeaseId())); - } - - final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); - abfsUriQueryBuilder.addQuery(QUERY_PARAM_ACTION, APPEND_ACTION); - abfsUriQueryBuilder.addQuery(QUERY_PARAM_POSITION, Long.toString(reqParams.getPosition())); - - if ((reqParams.getMode() == AppendRequestParameters.Mode.FLUSH_MODE) || ( - reqParams.getMode() == AppendRequestParameters.Mode.FLUSH_CLOSE_MODE)) { - abfsUriQueryBuilder.addQuery(QUERY_PARAM_FLUSH, TRUE); - if (reqParams.getMode() == AppendRequestParameters.Mode.FLUSH_CLOSE_MODE) { - abfsUriQueryBuilder.addQuery(QUERY_PARAM_CLOSE, TRUE); - } - } - - // Check if the retry is with "Expect: 100-continue" header being present in the previous request. - if (reqParams.isRetryDueToExpect()) { - String userAgentRetry = userAgent; - // Remove the specific marker related to "Expect: 100-continue" from the User-Agent string. - userAgentRetry = userAgentRetry.replace(HUNDRED_CONTINUE_USER_AGENT, EMPTY_STRING); - requestHeaders.removeIf(header -> header.getName().equalsIgnoreCase(USER_AGENT)); - requestHeaders.add(new AbfsHttpHeader(USER_AGENT, userAgentRetry)); - } - - // Add MD5 Hash of request content as request header if feature is enabled - if (isChecksumValidationEnabled()) { - addCheckSumHeaderForWrite(requestHeaders, reqParams, buffer); - } - - // AbfsInputStream/AbfsOutputStream reuse SAS tokens for better performance - String sasTokenForReuse = appendSASTokenToQuery(path, SASTokenProvider.WRITE_OPERATION, - abfsUriQueryBuilder, cachedSasToken); - - final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); - final AbfsRestOperation op = getAbfsRestOperation( - AbfsRestOperationType.Append, - HTTP_METHOD_PUT, - url, - requestHeaders, - buffer, - reqParams.getoffset(), - reqParams.getLength(), - sasTokenForReuse); - try { - op.execute(tracingContext); - } catch (AbfsRestOperationException e) { - /* - If the http response code indicates a user error we retry - the same append request with expect header being disabled. - When "100-continue" header is enabled but a non Http 100 response comes, - the response message might not get set correctly by the server. - So, this handling is to avoid breaking of backward compatibility - if someone has taken dependency on the exception message, - which is created using the error string present in the response header. - */ - int responseStatusCode = e.getStatusCode(); - if (checkUserError(responseStatusCode) && reqParams.isExpectHeaderEnabled()) { - LOG.debug("User error, retrying without 100 continue enabled for the given path {}", path); - reqParams.setExpectHeaderEnabled(false); - reqParams.setRetryDueToExpect(true); - return this.append(path, buffer, reqParams, cachedSasToken, - contextEncryptionAdapter, tracingContext); - } - // If we have no HTTP response, throw the original exception. - if (!op.hasResult()) { - throw e; - } - - if (isMd5ChecksumError(e)) { - throw new AbfsInvalidChecksumException(e); - } - - if (reqParams.isAppendBlob() - && appendSuccessCheckOp(op, path, - (reqParams.getPosition() + reqParams.getLength()), tracingContext)) { - final AbfsRestOperation successOp = getAbfsRestOperation( - AbfsRestOperationType.Append, - HTTP_METHOD_PUT, - url, - requestHeaders, - buffer, - reqParams.getoffset(), - reqParams.getLength(), - sasTokenForReuse); - successOp.hardSetResult(HttpURLConnection.HTTP_OK); - return successOp; - } - throw e; - } - - catch (AzureBlobFileSystemException e) { - // Any server side issue will be returned as AbfsRestOperationException and will be handled above. - LOG.debug("Append request failed with non server issues for path: {}, offset: {}, position: {}", - path, reqParams.getoffset(), reqParams.getPosition()); - throw e; - } - - return op; - } + /** + * Uploads data to be appended to a file. + * @param path to which data has to be appended. + * @param buffer containing data to be appended. + * @param reqParams containing parameters for append operation like offset, length etc. + * @param cachedSasToken to be used for the authenticating operation. + * @param contextEncryptionAdapter to provide encryption context. + * @param tracingContext for tracing the server calls. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation fails. + */ + public abstract AbfsRestOperation append(String path, byte[] buffer, + AppendRequestParameters reqParams, String cachedSasToken, + ContextEncryptionAdapter contextEncryptionAdapter, TracingContext tracingContext) + throws AzureBlobFileSystemException; /** * Returns true if the status code lies in the range of user error. * @param responseStatusCode http response status code. * @return True or False. */ - private boolean checkUserError(int responseStatusCode) { - return (responseStatusCode >= HttpURLConnection.HTTP_BAD_REQUEST - && responseStatusCode < HttpURLConnection.HTTP_INTERNAL_ERROR); - } + public abstract boolean checkUserError(int responseStatusCode); /** * To check if the failure exception returned by server is due to MD5 Mismatch * @param e Exception returned by AbfsRestOperation * @return boolean whether exception is due to MD5Mismatch or not */ - private boolean isMd5ChecksumError(final AbfsRestOperationException e) { + protected boolean isMd5ChecksumError(final AbfsRestOperationException e) { AzureServiceErrorCode storageErrorCode = e.getErrorCode(); return storageErrorCode == AzureServiceErrorCode.MD5_MISMATCH; } @@ -1040,16 +744,16 @@ private boolean isMd5ChecksumError(final AbfsRestOperationException e) { // However a retry would fail with an InvalidQueryParameterValue // (as the current offset would be unacceptable). // Hence, we pass/succeed the appendblob append call - // in case we are doing a retry after checking the length of the file + // in case we are doing a retry after checking the length of the file. public boolean appendSuccessCheckOp(AbfsRestOperation op, final String path, - final long length, TracingContext tracingContext) - throws AzureBlobFileSystemException { + final long length, TracingContext tracingContext) + throws AzureBlobFileSystemException { if ((op.isARetriedRequest()) - && (op.getResult().getStatusCode() == HttpURLConnection.HTTP_BAD_REQUEST)) { + && (op.getResult().getStatusCode() == HttpURLConnection.HTTP_BAD_REQUEST)) { final AbfsRestOperation destStatusOp = getPathStatus(path, false, tracingContext, null); if (destStatusOp.getResult().getStatusCode() == HttpURLConnection.HTTP_OK) { String fileLength = destStatusOp.getResult().getResponseHeader( - HttpHeaderConfigurations.CONTENT_LENGTH); + HttpHeaderConfigurations.CONTENT_LENGTH); if (length <= Long.parseLong(fileLength)) { LOG.debug("Returning success response from append blob idempotency code"); return true; @@ -1059,203 +763,111 @@ public boolean appendSuccessCheckOp(AbfsRestOperation op, final String path, return false; } - public AbfsRestOperation flush(final String path, final long position, - boolean retainUncommittedData, boolean isClose, - final String cachedSasToken, final String leaseId, - ContextEncryptionAdapter contextEncryptionAdapter, TracingContext tracingContext) - throws AzureBlobFileSystemException { - final List requestHeaders = createDefaultHeaders(); - addEncryptionKeyRequestHeaders(path, requestHeaders, false, - contextEncryptionAdapter, tracingContext); - // JDK7 does not support PATCH, so to workaround the issue we will use - // PUT and specify the real method in the X-Http-Method-Override header. - requestHeaders.add(new AbfsHttpHeader(X_HTTP_METHOD_OVERRIDE, - HTTP_METHOD_PATCH)); - if (leaseId != null) { - requestHeaders.add(new AbfsHttpHeader(X_MS_LEASE_ID, leaseId)); - } - - final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); - abfsUriQueryBuilder.addQuery(QUERY_PARAM_ACTION, FLUSH_ACTION); - abfsUriQueryBuilder.addQuery(QUERY_PARAM_POSITION, Long.toString(position)); - abfsUriQueryBuilder.addQuery(QUERY_PARAM_RETAIN_UNCOMMITTED_DATA, String.valueOf(retainUncommittedData)); - abfsUriQueryBuilder.addQuery(QUERY_PARAM_CLOSE, String.valueOf(isClose)); - - // AbfsInputStream/AbfsOutputStream reuse SAS tokens for better performance - String sasTokenForReuse = appendSASTokenToQuery(path, SASTokenProvider.WRITE_OPERATION, - abfsUriQueryBuilder, cachedSasToken); - - final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); - final AbfsRestOperation op = getAbfsRestOperation( - AbfsRestOperationType.Flush, - HTTP_METHOD_PUT, - url, - requestHeaders, sasTokenForReuse); - op.execute(tracingContext); - return op; - } - - public AbfsRestOperation setPathProperties(final String path, final String properties, - final TracingContext tracingContext, final ContextEncryptionAdapter contextEncryptionAdapter) - throws AzureBlobFileSystemException { - final List requestHeaders = createDefaultHeaders(); - addEncryptionKeyRequestHeaders(path, requestHeaders, false, - contextEncryptionAdapter, tracingContext); - // JDK7 does not support PATCH, so to workaround the issue we will use - // PUT and specify the real method in the X-Http-Method-Override header. - requestHeaders.add(new AbfsHttpHeader(X_HTTP_METHOD_OVERRIDE, - HTTP_METHOD_PATCH)); - - requestHeaders.add(new AbfsHttpHeader(X_MS_PROPERTIES, properties)); - - final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); - abfsUriQueryBuilder.addQuery(QUERY_PARAM_ACTION, SET_PROPERTIES_ACTION); - appendSASTokenToQuery(path, SASTokenProvider.SET_PROPERTIES_OPERATION, abfsUriQueryBuilder); - - final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); - final AbfsRestOperation op = getAbfsRestOperation( - AbfsRestOperationType.SetPathProperties, - HTTP_METHOD_PUT, - url, - requestHeaders); - op.execute(tracingContext); - return op; - } - - public AbfsRestOperation getPathStatus(final String path, - final boolean includeProperties, final TracingContext tracingContext, - final ContextEncryptionAdapter contextEncryptionAdapter) - throws AzureBlobFileSystemException { - final List requestHeaders = createDefaultHeaders(); - final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); - String operation = SASTokenProvider.GET_PROPERTIES_OPERATION; - if (!includeProperties) { - // The default action (operation) is implicitly to get properties and this action requires read permission - // because it reads user defined properties. If the action is getStatus or getAclStatus, then - // only traversal (execute) permission is required. - abfsUriQueryBuilder.addQuery(HttpQueryParams.QUERY_PARAM_ACTION, AbfsHttpConstants.GET_STATUS); - operation = SASTokenProvider.GET_STATUS_OPERATION; - } else { - addEncryptionKeyRequestHeaders(path, requestHeaders, false, - contextEncryptionAdapter, - tracingContext); - } - abfsUriQueryBuilder.addQuery(HttpQueryParams.QUERY_PARAM_UPN, String.valueOf(abfsConfiguration.isUpnUsed())); - appendSASTokenToQuery(path, operation, abfsUriQueryBuilder); - - final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); - final AbfsRestOperation op = getAbfsRestOperation( - AbfsRestOperationType.GetPathStatus, - HTTP_METHOD_HEAD, - url, - requestHeaders); - op.execute(tracingContext); - return op; - } - - public AbfsRestOperation read(final String path, - final long position, - final byte[] buffer, - final int bufferOffset, - final int bufferLength, - final String eTag, - String cachedSasToken, - ContextEncryptionAdapter contextEncryptionAdapter, - TracingContext tracingContext) throws AzureBlobFileSystemException { - final List requestHeaders = createDefaultHeaders(); - addEncryptionKeyRequestHeaders(path, requestHeaders, false, - contextEncryptionAdapter, tracingContext); - AbfsHttpHeader rangeHeader = new AbfsHttpHeader(RANGE, - String.format("bytes=%d-%d", position, position + bufferLength - 1)); - requestHeaders.add(rangeHeader); - requestHeaders.add(new AbfsHttpHeader(IF_MATCH, eTag)); - - // Add request header to fetch MD5 Hash of data returned by server. - if (isChecksumValidationEnabled(requestHeaders, rangeHeader, bufferLength)) { - requestHeaders.add(new AbfsHttpHeader(X_MS_RANGE_GET_CONTENT_MD5, TRUE)); - } - - final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); - - // AbfsInputStream/AbfsOutputStream reuse SAS tokens for better performance - String sasTokenForReuse = appendSASTokenToQuery(path, SASTokenProvider.READ_OPERATION, - abfsUriQueryBuilder, cachedSasToken); - - final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); - final AbfsRestOperation op = getAbfsRestOperation( - AbfsRestOperationType.ReadFile, - HTTP_METHOD_GET, - url, - requestHeaders, - buffer, - bufferOffset, - bufferLength, sasTokenForReuse); - op.execute(tracingContext); - - // Verify the MD5 hash returned by server holds valid on the data received. - if (isChecksumValidationEnabled(requestHeaders, rangeHeader, bufferLength)) { - verifyCheckSumForRead(buffer, op.getResult(), bufferOffset); - } - - return op; - } + /** + * Flush previously uploaded data to a file. + * @param path on which data has to be flushed. + * @param position to which data has to be flushed. + * @param retainUncommittedData whether to retain uncommitted data after flush. + * @param isClose specify if this is the last flush to the file. + * @param cachedSasToken to be used for the authenticating operation. + * @param leaseId if there is an active lease on the path. + * @param contextEncryptionAdapter to provide encryption context. + * @param tracingContext for tracing the server calls. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation fails. + */ + public abstract AbfsRestOperation flush(String path, long position, + boolean retainUncommittedData, boolean isClose, + String cachedSasToken, String leaseId, + ContextEncryptionAdapter contextEncryptionAdapter, TracingContext tracingContext) + throws AzureBlobFileSystemException; - public AbfsRestOperation deletePath(final String path, final boolean recursive, - final String continuation, - TracingContext tracingContext, - final boolean isNamespaceEnabled) - throws AzureBlobFileSystemException { - /* - * If Pagination is enabled and current API version is old, - * use the minimum required version for pagination. - * If Pagination is enabled and current API version is later than minimum required - * version for pagination, use current version only as azure service is backward compatible. - * If pagination is disabled, use the current API version only. - */ - final List requestHeaders = (isPaginatedDelete(recursive, - isNamespaceEnabled) && xMsVersion.compareTo(ApiVersion.AUG_03_2023) < 0) - ? createDefaultHeaders(ApiVersion.AUG_03_2023) - : createDefaultHeaders(); - final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); + /** + * Flush previously uploaded data to a file. + * @param buffer containing blockIds to be flushed. + * @param path on which data has to be flushed. + * @param isClose specify if this is the last flush to the file. + * @param cachedSasToken to be used for the authenticating operation. + * @param leaseId if there is an active lease on the path. + * @param eTag to specify conditional headers. + * @param tracingContext for tracing the server calls. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation fails. + */ + public abstract AbfsRestOperation flush(byte[] buffer, + String path, + boolean isClose, + String cachedSasToken, + String leaseId, + String eTag, + TracingContext tracingContext) throws AzureBlobFileSystemException; - if (isPaginatedDelete(recursive, isNamespaceEnabled)) { - // Add paginated query parameter - abfsUriQueryBuilder.addQuery(QUERY_PARAM_PAGINATED, TRUE); - } + /** + * Set the properties of a file or directory. + * @param path on which properties have to be set. + * @param properties list of metadata key-value pairs. + * @param tracingContext for tracing the server calls. + * @param contextEncryptionAdapter to provide encryption context. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation fails. + */ + public abstract AbfsRestOperation setPathProperties(String path, Hashtable properties, + TracingContext tracingContext, ContextEncryptionAdapter contextEncryptionAdapter) + throws AzureBlobFileSystemException; - abfsUriQueryBuilder.addQuery(QUERY_PARAM_RECURSIVE, String.valueOf(recursive)); - abfsUriQueryBuilder.addQuery(QUERY_PARAM_CONTINUATION, continuation); - String operation = recursive ? SASTokenProvider.DELETE_RECURSIVE_OPERATION : SASTokenProvider.DELETE_OPERATION; - appendSASTokenToQuery(path, operation, abfsUriQueryBuilder); + /** + * Get the properties of a file or directory. + * @param path of which properties have to be fetched. + * @param includeProperties to include user defined properties. + * @param tracingContext for tracing the server calls. + * @param contextEncryptionAdapter to provide encryption context. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation fails. + */ + public abstract AbfsRestOperation getPathStatus(String path, + boolean includeProperties, TracingContext tracingContext, + ContextEncryptionAdapter contextEncryptionAdapter) + throws AzureBlobFileSystemException; - final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); - final AbfsRestOperation op = new AbfsRestOperation( - AbfsRestOperationType.DeletePath, - this, - HTTP_METHOD_DELETE, - url, - requestHeaders, - abfsConfiguration); - try { - op.execute(tracingContext); - } catch (AzureBlobFileSystemException e) { - // If we have no HTTP response, throw the original exception. - if (!op.hasResult()) { - throw e; - } - final AbfsRestOperation idempotencyOp = deleteIdempotencyCheckOp(op); - if (idempotencyOp.getResult().getStatusCode() - == op.getResult().getStatusCode()) { - // idempotency did not return different result - // throw back the exception - throw e; - } else { - return idempotencyOp; - } - } + /** + * Read the contents of the file at specified path. + * @param path of the file to be read. + * @param position in the file from where data has to be read. + * @param buffer to store the data read. + * @param bufferOffset offset in the buffer to start storing the data. + * @param bufferLength length of data to be read. + * @param eTag to specify conditional headers. + * @param cachedSasToken to be used for the authenticating operation. + * @param contextEncryptionAdapter to provide encryption context. + * @param tracingContext for tracing the server calls. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation fails. + */ + public abstract AbfsRestOperation read(String path, + long position, + byte[] buffer, + int bufferOffset, + int bufferLength, + String eTag, + String cachedSasToken, + ContextEncryptionAdapter contextEncryptionAdapter, + TracingContext tracingContext) throws AzureBlobFileSystemException; - return op; - } + /** + * Delete the file or directory at specified path. + * @param path to be deleted. + * @param recursive if the path is a directory, delete recursively. + * @param continuation to specify continuation token. + * @param tracingContext for tracing the server calls. + * @param isNamespaceEnabled specify if the namespace is enabled. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation fails. + */ + public abstract AbfsRestOperation deletePath(String path, boolean recursive, + String continuation, + TracingContext tracingContext, + boolean isNamespaceEnabled) + throws AzureBlobFileSystemException; /** * Check if the delete request failure is post a retry and if delete failure @@ -1269,21 +881,21 @@ public AbfsRestOperation deletePath(final String path, final boolean recursive, * delete issued from this filesystem instance. * These are few corner cases and usually returning a success at this stage * should help the job to continue. - * @param op Delete request REST operation response with non-null HTTP response - * @return REST operation response post idempotency check + * @param op Delete request REST operation response with non-null HTTP response. + * @return REST operation response post idempotency check. */ public AbfsRestOperation deleteIdempotencyCheckOp(final AbfsRestOperation op) { Preconditions.checkArgument(op.hasResult(), "Operations has null HTTP response"); if ((op.isARetriedRequest()) - && (op.getResult().getStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) - && DEFAULT_DELETE_CONSIDERED_IDEMPOTENT) { + && (op.getResult().getStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) + && DEFAULT_DELETE_CONSIDERED_IDEMPOTENT) { // Server has returned HTTP 404, which means path no longer // exists. Assuming delete result to be idempotent, return success. final AbfsRestOperation successOp = getAbfsRestOperation( - AbfsRestOperationType.DeletePath, - HTTP_METHOD_DELETE, - op.getUrl(), - op.getRequestHeaders()); + AbfsRestOperationType.DeletePath, + HTTP_METHOD_DELETE, + op.getUrl(), + op.getRequestHeaders()); successOp.hardSetResult(HttpURLConnection.HTTP_OK); LOG.debug("Returning success response from delete idempotency logic"); return successOp; @@ -1292,117 +904,79 @@ public AbfsRestOperation deleteIdempotencyCheckOp(final AbfsRestOperation op) { return op; } - public AbfsRestOperation setOwner(final String path, final String owner, final String group, - TracingContext tracingContext) - throws AzureBlobFileSystemException { - final List requestHeaders = createDefaultHeaders(); - // JDK7 does not support PATCH, so to workaround the issue we will use - // PUT and specify the real method in the X-Http-Method-Override header. - requestHeaders.add(new AbfsHttpHeader(X_HTTP_METHOD_OVERRIDE, - HTTP_METHOD_PATCH)); - - if (owner != null && !owner.isEmpty()) { - requestHeaders.add(new AbfsHttpHeader(HttpHeaderConfigurations.X_MS_OWNER, owner)); - } - if (group != null && !group.isEmpty()) { - requestHeaders.add(new AbfsHttpHeader(HttpHeaderConfigurations.X_MS_GROUP, group)); - } - - final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); - abfsUriQueryBuilder.addQuery(HttpQueryParams.QUERY_PARAM_ACTION, AbfsHttpConstants.SET_ACCESS_CONTROL); - appendSASTokenToQuery(path, SASTokenProvider.SET_OWNER_OPERATION, abfsUriQueryBuilder); - - final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); - final AbfsRestOperation op = getAbfsRestOperation( - AbfsRestOperationType.SetOwner, - AbfsHttpConstants.HTTP_METHOD_PUT, - url, - requestHeaders); - op.execute(tracingContext); - return op; - } - - public AbfsRestOperation setPermission(final String path, final String permission, - TracingContext tracingContext) - throws AzureBlobFileSystemException { - final List requestHeaders = createDefaultHeaders(); - // JDK7 does not support PATCH, so to workaround the issue we will use - // PUT and specify the real method in the X-Http-Method-Override header. - requestHeaders.add(new AbfsHttpHeader(X_HTTP_METHOD_OVERRIDE, - HTTP_METHOD_PATCH)); - - requestHeaders.add(new AbfsHttpHeader(HttpHeaderConfigurations.X_MS_PERMISSIONS, permission)); - - final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); - abfsUriQueryBuilder.addQuery(HttpQueryParams.QUERY_PARAM_ACTION, AbfsHttpConstants.SET_ACCESS_CONTROL); - appendSASTokenToQuery(path, SASTokenProvider.SET_PERMISSION_OPERATION, abfsUriQueryBuilder); + /** + * Sets the owner on tha path. + * @param path on which owner has to be set. + * @param owner to be set. + * @param group to be set. + * @param tracingContext for tracing the server calls. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation fails. + */ + public abstract AbfsRestOperation setOwner(String path, String owner, String group, + TracingContext tracingContext) + throws AzureBlobFileSystemException; - final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); - final AbfsRestOperation op = getAbfsRestOperation( - AbfsRestOperationType.SetPermissions, - AbfsHttpConstants.HTTP_METHOD_PUT, - url, - requestHeaders); - op.execute(tracingContext); - return op; - } + /** + * Sets the permission on the path. + * @param path on which permission has to be set. + * @param permission to be set. + * @param tracingContext for tracing the server calls. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation fails. + */ + public abstract AbfsRestOperation setPermission(String path, String permission, + TracingContext tracingContext) + throws AzureBlobFileSystemException; + /** + * Sets the ACL. + * @param path on which ACL has to be set. + * @param aclSpecString to be set. + * @param tracingContext for tracing the server calls. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation fails. + */ public AbfsRestOperation setAcl(final String path, final String aclSpecString, TracingContext tracingContext) throws AzureBlobFileSystemException { - return setAcl(path, aclSpecString, AbfsHttpConstants.EMPTY_STRING, tracingContext); + return setAcl(path, aclSpecString, EMPTY_STRING, tracingContext); } - public AbfsRestOperation setAcl(final String path, final String aclSpecString, final String eTag, - TracingContext tracingContext) - throws AzureBlobFileSystemException { - final List requestHeaders = createDefaultHeaders(); - // JDK7 does not support PATCH, so to workaround the issue we will use - // PUT and specify the real method in the X-Http-Method-Override header. - requestHeaders.add(new AbfsHttpHeader(X_HTTP_METHOD_OVERRIDE, - HTTP_METHOD_PATCH)); - - requestHeaders.add(new AbfsHttpHeader(HttpHeaderConfigurations.X_MS_ACL, aclSpecString)); - - if (eTag != null && !eTag.isEmpty()) { - requestHeaders.add(new AbfsHttpHeader(HttpHeaderConfigurations.IF_MATCH, eTag)); - } - - final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); - abfsUriQueryBuilder.addQuery(HttpQueryParams.QUERY_PARAM_ACTION, AbfsHttpConstants.SET_ACCESS_CONTROL); - appendSASTokenToQuery(path, SASTokenProvider.SET_ACL_OPERATION, abfsUriQueryBuilder); - - final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); - final AbfsRestOperation op = getAbfsRestOperation( - AbfsRestOperationType.SetAcl, - AbfsHttpConstants.HTTP_METHOD_PUT, - url, - requestHeaders); - op.execute(tracingContext); - return op; - } + /** + * Sets the ACL on the path that matches ETag. + * @param path on which ACL has to be set. + * @param aclSpecString to be set. + * @param eTag to specify conditional headers. Set only if etag matches. + * @param tracingContext for tracing the server calls. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation fails. + */ + public abstract AbfsRestOperation setAcl(String path, String aclSpecString, String eTag, + TracingContext tracingContext) + throws AzureBlobFileSystemException; + /** + * Retrieves the ACL properties of blob at specified path. + * @param path of which properties have to be fetched. + * @param tracingContext for tracing the server calls. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation fails. + */ public AbfsRestOperation getAclStatus(final String path, TracingContext tracingContext) throws AzureBlobFileSystemException { return getAclStatus(path, abfsConfiguration.isUpnUsed(), tracingContext); } - public AbfsRestOperation getAclStatus(final String path, final boolean useUPN, - TracingContext tracingContext) throws AzureBlobFileSystemException { - final List requestHeaders = createDefaultHeaders(); - final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); - abfsUriQueryBuilder.addQuery(HttpQueryParams.QUERY_PARAM_ACTION, AbfsHttpConstants.GET_ACCESS_CONTROL); - abfsUriQueryBuilder.addQuery(HttpQueryParams.QUERY_PARAM_UPN, String.valueOf(useUPN)); - appendSASTokenToQuery(path, SASTokenProvider.GET_ACL_OPERATION, abfsUriQueryBuilder); - - final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); - final AbfsRestOperation op = getAbfsRestOperation( - AbfsRestOperationType.GetAcl, - AbfsHttpConstants.HTTP_METHOD_HEAD, - url, - requestHeaders); - op.execute(tracingContext); - return op; - } + /** + * Retrieves the ACL properties of blob at specified path. + * @param path of which properties have to be fetched. + * @param useUPN whether to use UPN with rest operation. + * @param tracingContext for tracing the server calls. + * @return executed rest operation containing response from server. + * @throws AzureBlobFileSystemException if rest operation fails. + */ + public abstract AbfsRestOperation getAclStatus(String path, boolean useUPN, + TracingContext tracingContext) throws AzureBlobFileSystemException; /** * Talks to the server to check whether the permission specified in @@ -1414,21 +988,8 @@ public AbfsRestOperation getAclStatus(final String path, final boolean useUPN, * @return The {@link AbfsRestOperation} object for the operation * @throws AzureBlobFileSystemException in case of bad requests */ - public AbfsRestOperation checkAccess(String path, String rwx, TracingContext tracingContext) - throws AzureBlobFileSystemException { - AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); - abfsUriQueryBuilder.addQuery(QUERY_PARAM_ACTION, CHECK_ACCESS); - abfsUriQueryBuilder.addQuery(QUERY_FS_ACTION, rwx); - appendSASTokenToQuery(path, SASTokenProvider.CHECK_ACCESS_OPERATION, abfsUriQueryBuilder); - URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); - AbfsRestOperation op = getAbfsRestOperation( - AbfsRestOperationType.CheckAccess, - AbfsHttpConstants.HTTP_METHOD_HEAD, - url, - createDefaultHeaders()); - op.execute(tracingContext); - return op; - } + public abstract AbfsRestOperation checkAccess(String path, String rwx, TracingContext tracingContext) + throws AzureBlobFileSystemException; /** * Get the directory query parameter used by the List Paths REST API and used @@ -1442,7 +1003,7 @@ public AbfsRestOperation checkAccess(String path, String rwx, TracingContext tra public static String getDirectoryQueryParameter(final String path) { String directory = path; if (Strings.isNullOrEmpty(directory)) { - directory = AbfsHttpConstants.EMPTY_STRING; + directory = EMPTY_STRING; } else if (directory.charAt(0) == '/') { directory = directory.substring(1); } @@ -1451,37 +1012,37 @@ public static String getDirectoryQueryParameter(final String path) { /** * If configured for SAS AuthType, appends SAS token to queryBuilder. - * @param path - * @param operation - * @param queryBuilder + * @param path for which SAS token is required. + * @param operation for which SAS token is required. + * @param queryBuilder to which SAS token is appended. * @return sasToken - returned for optional re-use. - * @throws SASTokenProviderException + * @throws SASTokenProviderException if SAS token cannot be acquired. */ - private String appendSASTokenToQuery(String path, String operation, AbfsUriQueryBuilder queryBuilder) throws SASTokenProviderException { + protected String appendSASTokenToQuery(String path, String operation, AbfsUriQueryBuilder queryBuilder) throws SASTokenProviderException { return appendSASTokenToQuery(path, operation, queryBuilder, null); } /** * If configured for SAS AuthType, appends SAS token to queryBuilder. - * @param path - * @param operation - * @param queryBuilder + * @param path for which SAS token is required. + * @param operation for which SAS token is required. + * @param queryBuilder to which SAS token is appended. * @param cachedSasToken - previously acquired SAS token to be reused. * @return sasToken - returned for optional re-use. - * @throws SASTokenProviderException + * @throws SASTokenProviderException if SAS token cannot be acquired. */ - private String appendSASTokenToQuery(String path, - String operation, - AbfsUriQueryBuilder queryBuilder, - String cachedSasToken) - throws SASTokenProviderException { + protected String appendSASTokenToQuery(String path, + String operation, + AbfsUriQueryBuilder queryBuilder, + String cachedSasToken) + throws SASTokenProviderException { String sasToken = null; if (this.authType == AuthType.SAS) { try { LOG.trace("Fetch SAS token for {} on {}", operation, path); if (cachedSasToken == null) { sasToken = sasTokenProvider.getSASToken(this.accountName, - this.filesystem, path, operation); + this.filesystem, path, operation); if ((sasToken == null) || sasToken.isEmpty()) { throw new UnsupportedOperationException("SASToken received is empty or null"); } @@ -1499,24 +1060,45 @@ private String appendSASTokenToQuery(String path, LOG.trace("SAS token fetch complete for {} on {}", operation, path); } catch (Exception ex) { throw new SASTokenProviderException(String.format( - "Failed to acquire a SAS token for %s on %s due to %s", operation, path, - ex.toString())); + "Failed to acquire a SAS token for %s on %s due to %s", operation, path, + ex.toString())); } } return sasToken; } + /** + * Creates REST operation URL with empty path for the given query. + * @param query to be added to the URL. + * @return URL for the REST operation. + * @throws AzureBlobFileSystemException if URL creation fails. + */ @VisibleForTesting - private URL createRequestUrl(final String query) throws AzureBlobFileSystemException { + protected URL createRequestUrl(final String query) throws AzureBlobFileSystemException { return createRequestUrl(EMPTY_STRING, query); } + /** + * Creates REST operation URL with given path and query. + * @param path for which URL has to be created. + * @param query to be added to the URL. + * @return URL for the REST operation. + * @throws AzureBlobFileSystemException if URL creation fails. + */ @VisibleForTesting protected URL createRequestUrl(final String path, final String query) throws AzureBlobFileSystemException { return createRequestUrl(baseUrl, path, query); } + /** + * Creates REST operation URL with given baseUrl, path and query. + * @param baseUrl to be used for the operation. + * @param path for which URL has to be created. + * @param query to be added to the URL. + * @return URL for the REST operation. + * @throws AzureBlobFileSystemException if URL creation fails. + */ @VisibleForTesting protected URL createRequestUrl(final URL baseUrl, final String path, final String query) throws AzureBlobFileSystemException { @@ -1545,14 +1127,20 @@ protected URL createRequestUrl(final URL baseUrl, final String path, final Strin return url; } + /** + * returns the url encoded string for a given value. + * @param value to be encoded. + * @return url encoded string. + * @throws AzureBlobFileSystemException if encoding fails. + */ public static String urlEncode(final String value) throws AzureBlobFileSystemException { String encodedString; try { encodedString = URLEncoder.encode(value, UTF_8) - .replace(PLUS, PLUS_ENCODE) - .replace(FORWARD_SLASH_ENCODE, FORWARD_SLASH); + .replace(PLUS, PLUS_ENCODE) + .replace(FORWARD_SLASH_ENCODE, FORWARD_SLASH); } catch (UnsupportedEncodingException ex) { - throw new InvalidUriException(value); + throw new InvalidUriException(value); } return encodedString; @@ -1570,7 +1158,7 @@ protected Boolean getIsPaginatedDeleteEnabled() { return abfsConfiguration.isPaginatedDeleteEnabled(); } - private Boolean isPaginatedDelete(boolean isRecursiveDelete, boolean isNamespaceEnabled) { + protected Boolean isPaginatedDelete(boolean isRecursiveDelete, boolean isNamespaceEnabled) { return getIsPaginatedDeleteEnabled() && isNamespaceEnabled && isRecursiveDelete; } @@ -1584,7 +1172,7 @@ public EncryptionContextProvider getEncryptionContextProvider() { @VisibleForTesting String initializeUserAgent(final AbfsConfiguration abfsConfiguration, - final String sslProviderName) { + final String sslProviderName) { StringBuilder sb = new StringBuilder(); @@ -1596,7 +1184,7 @@ String initializeUserAgent(final AbfsConfiguration abfsConfiguration, sb.append("("); sb.append(System.getProperty(JAVA_VENDOR) - .replaceAll(SINGLE_WHITE_SPACE, EMPTY_STRING)); + .replaceAll(SINGLE_WHITE_SPACE, EMPTY_STRING)); sb.append(SINGLE_WHITE_SPACE); sb.append("JavaJRE"); sb.append(SINGLE_WHITE_SPACE); @@ -1605,7 +1193,7 @@ String initializeUserAgent(final AbfsConfiguration abfsConfiguration, sb.append(SINGLE_WHITE_SPACE); sb.append(System.getProperty(OS_NAME) - .replaceAll(SINGLE_WHITE_SPACE, EMPTY_STRING)); + .replaceAll(SINGLE_WHITE_SPACE, EMPTY_STRING)); sb.append(SINGLE_WHITE_SPACE); sb.append(System.getProperty(OS_VERSION)); sb.append(FORWARD_SLASH); @@ -1614,7 +1202,7 @@ String initializeUserAgent(final AbfsConfiguration abfsConfiguration, appendIfNotEmpty(sb, sslProviderName, true); appendIfNotEmpty(sb, - ExtensionHelper.getUserAgentSuffix(tokenProvider, EMPTY_STRING), true); + ExtensionHelper.getUserAgentSuffix(tokenProvider, EMPTY_STRING), true); if (abfsConfiguration.isExpectHeaderEnabled()) { sb.append(SINGLE_WHITE_SPACE); @@ -1622,8 +1210,8 @@ String initializeUserAgent(final AbfsConfiguration abfsConfiguration, sb.append(SEMICOLON); } sb.append(SINGLE_WHITE_SPACE) - .append(abfsConfiguration.getPreferredHttpOperationType()) - .append(SEMICOLON); + .append(abfsConfiguration.getPreferredHttpOperationType()) + .append(SEMICOLON); sb.append(SINGLE_WHITE_SPACE); sb.append(abfsConfiguration.getClusterName()); @@ -1638,7 +1226,7 @@ String initializeUserAgent(final AbfsConfiguration abfsConfiguration, } private void appendIfNotEmpty(StringBuilder sb, String regEx, - boolean shouldAppendSemiColon) { + boolean shouldAppendSemiColon) { if (regEx == null || regEx.trim().isEmpty()) { return; } @@ -1656,11 +1244,11 @@ private void appendIfNotEmpty(StringBuilder sb, String regEx, * @param buffer for getting input data for MD5 computation * @throws AbfsRestOperationException if Md5 computation fails */ - private void addCheckSumHeaderForWrite(List requestHeaders, - final AppendRequestParameters reqParams, final byte[] buffer) - throws AbfsRestOperationException { + protected void addCheckSumHeaderForWrite(List requestHeaders, + final AppendRequestParameters reqParams, final byte[] buffer) + throws AbfsRestOperationException { String md5Hash = computeMD5Hash(buffer, reqParams.getoffset(), - reqParams.getLength()); + reqParams.getLength()); requestHeaders.add(new AbfsHttpHeader(CONTENT_MD5, md5Hash)); } @@ -1671,9 +1259,9 @@ private void addCheckSumHeaderForWrite(List requestHeaders, * @param bufferOffset Position where data returned by server is saved in buffer. * @throws AbfsRestOperationException if Md5Mismatch. */ - private void verifyCheckSumForRead(final byte[] buffer, - final AbfsHttpOperation result, final int bufferOffset) - throws AbfsRestOperationException { + protected void verifyCheckSumForRead(final byte[] buffer, + final AbfsHttpOperation result, final int bufferOffset) + throws AbfsRestOperationException { // Number of bytes returned by server could be less than or equal to what // caller requests. In case it is less, extra bytes will be initialized to 0 // Server returned MD5 Hash will be computed on what server returned. @@ -1684,7 +1272,7 @@ private void verifyCheckSumForRead(final byte[] buffer, return; } String md5HashComputed = computeMD5Hash(buffer, bufferOffset, - numberOfBytesRead); + numberOfBytesRead); String md5HashActual = result.getResponseHeader(CONTENT_MD5); if (!md5HashComputed.equals(md5HashActual)) { LOG.debug("Md5 Mismatch Error in Read Operation. Server returned Md5: {}, Client computed Md5: {}", md5HashActual, md5HashComputed); @@ -1694,9 +1282,8 @@ private void verifyCheckSumForRead(final byte[] buffer, /** * Conditions check for allowing checksum support for read operation. - * Sending MD5 Hash in request headers. For more details see - * @see
    - * Path - Read Azure Storage Rest API. + * Sending MD5 Hash in request headers. For more details refer to + * Path - Read Azure Storage Rest API. * 1. Range header must be present as one of the request headers. * 2. buffer length must be less than or equal to 4 MB. * @param requestHeaders to be checked for range header. @@ -1704,21 +1291,20 @@ private void verifyCheckSumForRead(final byte[] buffer, * @param bufferLength must be less than or equal to 4 MB. * @return true if all conditions are met. */ - private boolean isChecksumValidationEnabled(List requestHeaders, - final AbfsHttpHeader rangeHeader, final int bufferLength) { + protected boolean isChecksumValidationEnabled(List requestHeaders, + final AbfsHttpHeader rangeHeader, final int bufferLength) { return getAbfsConfiguration().getIsChecksumValidationEnabled() - && requestHeaders.contains(rangeHeader) && bufferLength <= 4 * ONE_MB; + && requestHeaders.contains(rangeHeader) && bufferLength <= 4 * ONE_MB; } /** * Conditions check for allowing checksum support for write operation. * Server will support this if client sends the MD5 Hash as a request header. - * For azure stoage service documentation see - * @see - * Path - Update Azure Rest API. + * For azure stoage service documentation and more details refer to + * Path - Update Azure Rest API. * @return true if checksum validation enabled. */ - private boolean isChecksumValidationEnabled() { + protected boolean isChecksumValidationEnabled() { return getAbfsConfiguration().getIsChecksumValidationEnabled(); } @@ -1732,7 +1318,7 @@ private boolean isChecksumValidationEnabled() { */ @VisibleForTesting public String computeMD5Hash(final byte[] data, final int off, final int len) - throws AbfsRestOperationException { + throws AbfsRestOperationException { try { MessageDigest md5Digest = MessageDigest.getInstance(MD5); md5Digest.update(data, off, len); @@ -1783,7 +1369,7 @@ public int getNumLeaseThreads() { } public ListenableScheduledFuture schedule(Callable callable, long delay, - TimeUnit timeUnit) { + TimeUnit timeUnit) { return executorService.schedule(callable, delay, timeUnit); } @@ -1818,10 +1404,10 @@ private TracingContext getMetricTracingContext() { hostName = "UnknownHost"; } return new TracingContext(TracingContext.validateClientCorrelationID( - abfsConfiguration.getClientCorrelationId()), - hostName, FSOperationType.GET_ATTR, true, - abfsConfiguration.getTracingHeaderFormat(), - null, abfsCounters.toString()); + abfsConfiguration.getClientCorrelationId()), + hostName, FSOperationType.GET_ATTR, true, + abfsConfiguration.getTracingHeaderFormat(), + null, abfsCounters.toString()); } /** @@ -1833,7 +1419,7 @@ private TracingContext getMetricTracingContext() { boolean timerOrchestrator(TimerFunctionality timerFunctionality, TimerTask timerTask) { switch (timerFunctionality) { case RESUME: - if (isMetricCollectionStopped.get()) { + if (isMetricCollectionEnabled && isMetricCollectionStopped.get()) { synchronized (this) { if (isMetricCollectionStopped.get()) { resumeTimer(); @@ -1864,8 +1450,8 @@ boolean timerOrchestrator(TimerFunctionality timerFunctionality, TimerTask timer private void resumeTimer() { isMetricCollectionStopped.set(false); timer.schedule(new TimerTaskImpl(), - metricIdlePeriod, - metricIdlePeriod); + metricIdlePeriod, + metricIdlePeriod); } /** @@ -1911,11 +1497,11 @@ class TimerTaskImpl extends TimerTask { public void run() { try { if (timerOrchestrator(TimerFunctionality.SUSPEND, this)) { - try { - getMetricCall(getMetricTracingContext()); - } finally { - abfsCounters.initializeMetrics(metricFormat); - } + try { + getMetricCall(getMetricTracingContext()); + } finally { + abfsCounters.initializeMetrics(metricFormat); + } } } catch (IOException e) { } @@ -1936,24 +1522,24 @@ public void run() { * @return An AbfsRestOperation instance. */ AbfsRestOperation getAbfsRestOperation(final AbfsRestOperationType operationType, - final String httpMethod, - final URL url, - final List requestHeaders, - final byte[] buffer, - final int bufferOffset, - final int bufferLength, - final String sasTokenForReuse) { + final String httpMethod, + final URL url, + final List requestHeaders, + final byte[] buffer, + final int bufferOffset, + final int bufferLength, + final String sasTokenForReuse) { return new AbfsRestOperation( - operationType, - this, - httpMethod, - url, - requestHeaders, - buffer, - bufferOffset, - bufferLength, - sasTokenForReuse, - abfsConfiguration); + operationType, + this, + httpMethod, + url, + requestHeaders, + buffer, + bufferOffset, + bufferLength, + sasTokenForReuse, + abfsConfiguration); } /** @@ -1966,16 +1552,16 @@ AbfsRestOperation getAbfsRestOperation(final AbfsRestOperationType operationType * @return An AbfsRestOperation instance. */ AbfsRestOperation getAbfsRestOperation(final AbfsRestOperationType operationType, - final String httpMethod, - final URL url, - final List requestHeaders) { + final String httpMethod, + final URL url, + final List requestHeaders) { return new AbfsRestOperation( - operationType, - this, - httpMethod, - url, - requestHeaders, - abfsConfiguration + operationType, + this, + httpMethod, + url, + requestHeaders, + abfsConfiguration ); } @@ -1990,16 +1576,16 @@ AbfsRestOperation getAbfsRestOperation(final AbfsRestOperationType operationType * @return An AbfsRestOperation instance. */ AbfsRestOperation getAbfsRestOperation(final AbfsRestOperationType operationType, - final String httpMethod, - final URL url, - final List requestHeaders, - final String sasTokenForReuse) { + final String httpMethod, + final URL url, + final List requestHeaders, + final String sasTokenForReuse) { return new AbfsRestOperation( - operationType, - this, - httpMethod, - url, - requestHeaders, sasTokenForReuse, abfsConfiguration); + operationType, + this, + httpMethod, + url, + requestHeaders, sasTokenForReuse, abfsConfiguration); } @VisibleForTesting @@ -2011,4 +1597,17 @@ AbfsApacheHttpClient getAbfsApacheHttpClient() { KeepAliveCache getKeepAliveCache() { return keepAliveCache; } -} + + @VisibleForTesting + protected Timer getTimer() { + return timer; + } + + protected String getUserAgent() { + return userAgent; + } + + protected boolean isRenameResilience() { + return renameResilience; + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java index 35c896c4d7007..152a52ff10b37 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java @@ -28,8 +28,12 @@ import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.DEFAULT_READ_BUFFER_SIZE; import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.NON_PARQUET; import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.PARQUET; -import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.*; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.FILE_LENGTH; import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.READ_COUNT; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.READ_LEN_REQUESTED; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.SIZE_READ_BY_FIRST_READ; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.TOTAL_FILES; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; From 8a87b39dc96c616a1a97cd36c7c0bd193c8f0e14 Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Thu, 17 Oct 2024 02:39:40 -0700 Subject: [PATCH 09/32] Abfs Client format fix --- .../java/org/apache/hadoop/fs/azurebfs/services/AbfsClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClient.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClient.java index 2093e26d8a47a..d77280647ea8e 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClient.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClient.java @@ -1610,4 +1610,4 @@ protected String getUserAgent() { protected boolean isRenameResilience() { return renameResilience; } -} \ No newline at end of file +} From fcaf09f3dac64bd98ba59aeacc5d46704e4b3d45 Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Thu, 17 Oct 2024 12:41:56 -0700 Subject: [PATCH 10/32] Files refactor --- .../hadoop/fs/azurebfs/AbfsCountersImpl.java | 3 +- .../azurebfs/constants/MetricsConstants.java | 9 +- .../enums/AbfsBackoffMetricsEnum.java | 44 +-- .../enums/AbfsReadFooterMetricsEnum.java | 29 +- .../hadoop/fs/azurebfs/enums/FileType.java | 12 + .../hadoop/fs/azurebfs/enums/RetryValue.java | 39 +++ .../fs/azurebfs/enums/StatisticTypeEnum.java | 12 + .../azurebfs/services/AbfsBackoffMetrics.java | 271 +++++++----------- .../services/AbfsReadFooterMetrics.java | 104 +++---- .../azurebfs/services/AbfsRestOperation.java | 74 +++-- .../AbstractAbfsStatisticsSource.java | 24 +- .../azurebfs/ITestAbfsReadFooterMetrics.java | 24 +- .../services/TestAbfsRestOperation.java | 4 +- 13 files changed, 344 insertions(+), 305 deletions(-) create mode 100644 hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/FileType.java create mode 100644 hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java create mode 100644 hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/StatisticTypeEnum.java diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java index 2c67feb162cf5..7727f29ba3c0f 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java @@ -70,6 +70,7 @@ import static org.apache.hadoop.fs.azurebfs.AbfsStatistic.SEND_REQUESTS; import static org.apache.hadoop.fs.azurebfs.AbfsStatistic.SERVER_UNAVAILABLE; import static org.apache.hadoop.fs.azurebfs.AbfsStatistic.WRITE_THROTTLES; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.TOTAL_NUMBER_OF_REQUESTS; import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.iostatisticsStore; import static org.apache.hadoop.util.Time.now; @@ -325,7 +326,7 @@ public DurationTracker trackDuration(String key) { public String toString() { String metric = ""; if (abfsBackoffMetrics != null) { - long totalNoRequests = getAbfsBackoffMetrics().getTotalNumberOfRequests(); + long totalNoRequests = getAbfsBackoffMetrics().getMetricValue(TOTAL_NUMBER_OF_REQUESTS); if (totalNoRequests > 0) { metric += "#BO:" + getAbfsBackoffMetrics().toString(); } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java index 4934725f10149..94e95e8e41b0f 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java @@ -18,16 +18,9 @@ package org.apache.hadoop.fs.azurebfs.constants; -import java.util.Arrays; -import java.util.List; - public final class MetricsConstants { public static final String COLON = ":"; public static final String RETRY = "RETRY"; public static final String BASE = "BASE"; - public static final List RETRY_LIST = Arrays.asList("1", "2", "3", "4", "5_15", "15_25", "25AndAbove"); - public static final String COUNTER = "COUNTER"; - public static final String GAUGE = "GAUGE"; - public static final String PARQUET = "PARQUET"; - public static final String NON_PARQUET = "NON_PARQUET"; + public static final String FILE = "FILE"; } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java index 273d5f50b0c77..d1c1cb2af6f9d 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java @@ -20,41 +20,49 @@ import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.BASE; import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.RETRY; +import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_COUNTER; +import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_GAUGE; public enum AbfsBackoffMetricsEnum { - NUMBER_OF_IOPS_THROTTLED_REQUESTS("numberOfIOPSThrottledRequests", BASE, "Number of IOPS throttled requests"), - NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS("numberOfBandwidthThrottledRequests", BASE, "Number of bandwidth throttled requests"), - NUMBER_OF_OTHER_THROTTLED_REQUESTS("numberOfOtherThrottledRequests", BASE, "Number of other throttled requests"), - NUMBER_OF_NETWORK_FAILED_REQUESTS("numberOfNetworkFailedRequests", BASE, "Number of network failed requests"), - MAX_RETRY_COUNT("maxRetryCount", BASE, "Max retry count"), - TOTAL_NUMBER_OF_REQUESTS("totalNumberOfRequests", BASE, "Total number of requests"), - NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING("numberOfRequestsSucceededWithoutRetrying", BASE, "Number of requests succeeded without retrying"), - NUMBER_OF_REQUESTS_FAILED("numberOfRequestsFailed", BASE, "Number of requests failed"), - NUMBER_OF_REQUESTS_SUCCEEDED("numberOfRequestsSucceeded", RETRY,"Number of requests succeeded"), - MIN_BACK_OFF("minBackOff", RETRY, "Minimum backoff"), - MAX_BACK_OFF("maxBackOff", RETRY, "Maximum backoff"), - TOTAL_BACK_OFF("totalBackoff", RETRY, "Total backoff"), - TOTAL_REQUESTS("totalRequests", RETRY, "Total requests"); + NUMBER_OF_IOPS_THROTTLED_REQUESTS("numberOfIOPSThrottledRequests", "Number of IOPS throttled requests", BASE, TYPE_COUNTER), + NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS("numberOfBandwidthThrottledRequests", "Number of bandwidth throttled requests", BASE, TYPE_COUNTER), + NUMBER_OF_OTHER_THROTTLED_REQUESTS("numberOfOtherThrottledRequests", "Number of other throttled requests", BASE, TYPE_COUNTER), + NUMBER_OF_NETWORK_FAILED_REQUESTS("numberOfNetworkFailedRequests", "Number of network failed requests", BASE, TYPE_COUNTER), + MAX_RETRY_COUNT("maxRetryCount", "Max retry count", BASE, TYPE_COUNTER), + TOTAL_NUMBER_OF_REQUESTS("totalNumberOfRequests", "Total number of requests", BASE, TYPE_COUNTER), + NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING("numberOfRequestsSucceededWithoutRetrying", "Number of requests succeeded without retrying", BASE, TYPE_COUNTER), + NUMBER_OF_REQUESTS_FAILED("numberOfRequestsFailed", "Number of requests failed", BASE, TYPE_COUNTER), + NUMBER_OF_REQUESTS_SUCCEEDED("numberOfRequestsSucceeded", "Number of requests succeeded", RETRY, TYPE_COUNTER), + MIN_BACK_OFF("minBackOff", "Minimum backoff", RETRY, TYPE_GAUGE), + MAX_BACK_OFF("maxBackOff", "Maximum backoff", RETRY, TYPE_GAUGE), + TOTAL_BACK_OFF("totalBackoff", "Total backoff", RETRY, TYPE_GAUGE), + TOTAL_REQUESTS("totalRequests", "Total requests", RETRY, TYPE_COUNTER); private final String name; - private final String type; private final String description; + private final String type; + private final StatisticTypeEnum statisticType; - AbfsBackoffMetricsEnum(String name, String type, String description) { + AbfsBackoffMetricsEnum(String name, String description, String type, StatisticTypeEnum statisticType) { this.name = name; - this.type = type; this.description = description; + this.type = type; + this.statisticType = statisticType; } public String getName() { return name; } + public String getDescription() { + return description; + } + public String getType() { return type; } - public String getDescription() { - return description; + public StatisticTypeEnum getStatisticType() { + return statisticType; } } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java index 1b7bb917506ef..ca9f9b927228e 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java @@ -18,27 +18,30 @@ package org.apache.hadoop.fs.azurebfs.enums; -import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.COUNTER; -import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.GAUGE; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.FILE; +import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_COUNTER; +import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_GAUGE; public enum AbfsReadFooterMetricsEnum { - TOTAL_FILES("totalFiles", "Total files in a file system", COUNTER), - FILE_LENGTH("fileLength", "File length", GAUGE), - SIZE_READ_BY_FIRST_READ("sizeReadByFirstRead", "Size read by first read", GAUGE), - OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ("offsetDiffBetweenFirstAndSecondRead", "Offset difference between first and second read", GAUGE), - READ_LEN_REQUESTED("readLenRequested", "Read length requested", GAUGE), - READ_COUNT("readCount", "Read count", COUNTER), - FIRST_OFFSET_DIFF("firstOffsetDiff", "First offset difference", GAUGE), - SECOND_OFFSET_DIFF("secondOffsetDiff", "Second offset difference", GAUGE); + TOTAL_FILES("totalFiles", "Total files in a file system", FILE, TYPE_COUNTER), + FILE_LENGTH("fileLength", "File length", FILE, TYPE_GAUGE), + SIZE_READ_BY_FIRST_READ("sizeReadByFirstRead", "Size read by first read", FILE, TYPE_GAUGE), + OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ("offsetDiffBetweenFirstAndSecondRead", "Offset difference between first and second read", FILE, TYPE_GAUGE), + READ_LEN_REQUESTED("readLenRequested", "Read length requested", FILE, TYPE_GAUGE), + READ_COUNT("readCount", "Read count", FILE, TYPE_COUNTER), + FIRST_OFFSET_DIFF("firstOffsetDiff", "First offset difference", FILE, TYPE_GAUGE), + SECOND_OFFSET_DIFF("secondOffsetDiff", "Second offset difference", FILE, TYPE_GAUGE); private final String name; private final String description; private final String type; + private final StatisticTypeEnum statisticType; - AbfsReadFooterMetricsEnum(String name, String description, String type) { + AbfsReadFooterMetricsEnum(String name, String description, String type, StatisticTypeEnum statisticType) { this.name = name; this.description = description; this.type = type; + this.statisticType = statisticType; } public String getName() { @@ -52,4 +55,8 @@ public String getDescription() { public String getType() { return type; } + + public StatisticTypeEnum getStatisticType() { + return statisticType; + } } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/FileType.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/FileType.java new file mode 100644 index 0000000000000..a51a39cee1c19 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/FileType.java @@ -0,0 +1,12 @@ +package org.apache.hadoop.fs.azurebfs.enums; + +public enum FileType { + /** + * Parquet file. + */ + PARQUET, + /** + * Non-parquet file. + */ + NON_PARQUET +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java new file mode 100644 index 0000000000000..1f61a7c20e2c0 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java @@ -0,0 +1,39 @@ +package org.apache.hadoop.fs.azurebfs.enums; + +public enum RetryValue { + ONE("1"), + TWO("2"), + THREE("3"), + FOUR("4"), + FIVE_FIFTEEN("5_15"), + FIFTEEN_TWENTY_FIVE("15_25"), + TWENTY_FIVE_AND_ABOVE("25AndAbove"); + + private final String value; + + RetryValue(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static RetryValue getRetryValue(int retryCount) { + if (retryCount == 1) { + return ONE; + } else if (retryCount == 2) { + return TWO; + } else if (retryCount == 3) { + return THREE; + } else if (retryCount == 4) { + return FOUR; + } else if (retryCount >= 5 && retryCount < 15) { + return FIVE_FIFTEEN; + } else if (retryCount >= 15 && retryCount < 25) { + return FIFTEEN_TWENTY_FIVE; + } else { + return TWENTY_FIVE_AND_ABOVE; + } + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/StatisticTypeEnum.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/StatisticTypeEnum.java new file mode 100644 index 0000000000000..ab339b42e954e --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/StatisticTypeEnum.java @@ -0,0 +1,12 @@ +package org.apache.hadoop.fs.azurebfs.enums; + +public enum StatisticTypeEnum { + /** + * Counter. + */ + TYPE_COUNTER, + /** + * Gauge. + */ + TYPE_GAUGE +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java index ae2cde87a7230..18ec1e3c02b59 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java @@ -19,217 +19,164 @@ package org.apache.hadoop.fs.azurebfs.services; import java.util.Arrays; +import java.util.List; import java.util.stream.Stream; import org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum; +import org.apache.hadoop.fs.azurebfs.enums.RetryValue; +import org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum; import org.apache.hadoop.fs.azurebfs.statistics.AbstractAbfsStatisticsSource; import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.HUNDRED; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.THOUSAND; import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.RETRY; -import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.RETRY_LIST; import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.COLON; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.MAX_BACK_OFF; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.MIN_BACK_OFF; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.MAX_RETRY_COUNT; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.NUMBER_OF_IOPS_THROTTLED_REQUESTS; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.NUMBER_OF_NETWORK_FAILED_REQUESTS; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.NUMBER_OF_OTHER_THROTTLED_REQUESTS; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_FAILED; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.TOTAL_BACK_OFF; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.TOTAL_REQUESTS; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.TOTAL_NUMBER_OF_REQUESTS; +import static org.apache.hadoop.fs.azurebfs.enums.RetryValue.ONE; +import static org.apache.hadoop.fs.azurebfs.enums.RetryValue.TWO; +import static org.apache.hadoop.fs.azurebfs.enums.RetryValue.THREE; +import static org.apache.hadoop.fs.azurebfs.enums.RetryValue.FOUR; +import static org.apache.hadoop.fs.azurebfs.enums.RetryValue.FIVE_FIFTEEN; +import static org.apache.hadoop.fs.azurebfs.enums.RetryValue.FIFTEEN_TWENTY_FIVE; +import static org.apache.hadoop.fs.azurebfs.enums.RetryValue.TWENTY_FIVE_AND_ABOVE; +import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_COUNTER; +import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_GAUGE; import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.iostatisticsStore; public class AbfsBackoffMetrics extends AbstractAbfsStatisticsSource { + private static final List RETRY_LIST = Arrays.asList(ONE, TWO, THREE, FOUR, FIVE_FIFTEEN, FIFTEEN_TWENTY_FIVE, TWENTY_FIVE_AND_ABOVE); public AbfsBackoffMetrics() { IOStatisticsStore ioStatisticsStore = iostatisticsStore() - .withCounters(getCountersName()) + .withCounters(getMetricNames(TYPE_COUNTER)) + .withCounters(getMetricNames(TYPE_GAUGE)) .build(); setIOStatistics(ioStatisticsStore); } - private String[] getCountersName() { + private String[] getMetricNames(StatisticTypeEnum type) { return Arrays.stream(AbfsBackoffMetricsEnum.values()) + .filter(backoffMetricsEnum -> backoffMetricsEnum.getStatisticType().equals(type)) .flatMap(backoffMetricsEnum -> RETRY.equals(backoffMetricsEnum.getType()) ? RETRY_LIST.stream().map(retryCount -> retryCount + COLON + backoffMetricsEnum.getName()) : - Stream.of(backoffMetricsEnum.getName())) - .toArray(String[]::new); + Stream.of(backoffMetricsEnum.getName()) + ).toArray(String[]::new); } - public void incrementCounter(AbfsBackoffMetricsEnum metric, String retryCount, long value) { - incCounter(retryCount + COLON + metric.getName(), value); - } - - public void incrementCounter(AbfsBackoffMetricsEnum metric, String retryCount) { - incrementCounter(metric, retryCount, 1); - } - - public void incrementCounter(AbfsBackoffMetricsEnum metric, long value) { - incCounter(metric.getName(), value); - } - - public void incrementCounter(AbfsBackoffMetricsEnum metric) { - incrementCounter(metric, 1); - } - - public Long getCounter(AbfsBackoffMetricsEnum metric, String retryCount) { - return lookupCounterValue(retryCount + COLON + metric.getName()); - } - - public Long getCounter(AbfsBackoffMetricsEnum metric) { - return lookupCounterValue(metric.getName()); - } - - public void setCounter(AbfsBackoffMetricsEnum metric, String retryCount, long value) { - setCounterValue(retryCount + COLON + metric.getName(), value); - } - - public void setCounter(AbfsBackoffMetricsEnum metric, long value) { - setCounterValue(metric.getName(), value); - } - - public long getNumberOfIOPSThrottledRequests() { - return getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_IOPS_THROTTLED_REQUESTS); - } - - public void incrementNumberOfIOPSThrottledRequests() { - incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_IOPS_THROTTLED_REQUESTS); - } - - public long getNumberOfBandwidthThrottledRequests() { - return getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS); - } - - public void incrementNumberOfBandwidthThrottledRequests() { - incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS); - } - - public long getNumberOfOtherThrottledRequests() { - return getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_OTHER_THROTTLED_REQUESTS); - } - - public void incrementNumberOfOtherThrottledRequests() { - incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_OTHER_THROTTLED_REQUESTS); - } - - public long getNumberOfNetworkFailedRequests() { - return getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_NETWORK_FAILED_REQUESTS); - } - - public void incrementNumberOfNetworkFailedRequests() { - incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_NETWORK_FAILED_REQUESTS); - } - - public long getMaxRetryCount() { - return getCounter(AbfsBackoffMetricsEnum.MAX_RETRY_COUNT); - } - - public void setMaxRetryCount(long value) { - setCounter(AbfsBackoffMetricsEnum.MAX_RETRY_COUNT, value); - } - - public long getTotalNumberOfRequests() { - return getCounter(AbfsBackoffMetricsEnum.TOTAL_NUMBER_OF_REQUESTS); - } - - public void incrementTotalNumberOfRequests() { - incrementCounter(AbfsBackoffMetricsEnum.TOTAL_NUMBER_OF_REQUESTS); - } - - public long getNumberOfRequestsSucceededWithoutRetrying() { - return getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING); - } - - public void incrementNumberOfRequestsSucceededWithoutRetrying() { - incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING); - } - - public long getNumberOfRequestsFailed() { - return getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_FAILED); - } - - public void incrementNumberOfRequestsFailed() { - incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_FAILED); - } - - public long getNumberOfRequestsSucceeded(String retryCount) { - return getCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED, retryCount); - } - - public void incrementNumberOfRequestsSucceeded(String retryCount) { - incrementCounter(AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED, retryCount); - } - - public long getMinBackoff(String retryCount) { - return getCounter(AbfsBackoffMetricsEnum.MIN_BACK_OFF, retryCount); - } - - public void setMinBackoff(String retryCount, long value) { - setCounter(AbfsBackoffMetricsEnum.MIN_BACK_OFF, retryCount, value); - } - - public long getMaxBackoff(String retryCount) { - return getCounter(AbfsBackoffMetricsEnum.MAX_BACK_OFF, retryCount); + private String getMetricName(AbfsBackoffMetricsEnum metric, RetryValue retryValue) { + if (RETRY.equals(metric.getType())) { + return retryValue + COLON + metric.getName(); + } + return metric.getName(); + } + + public long getMetricValue(AbfsBackoffMetricsEnum metric, RetryValue retryValue) { + String metricName = getMetricName(metric, retryValue); + switch (metric.getStatisticType()) { + case TYPE_COUNTER: + return lookupCounterValue(metricName); + case TYPE_GAUGE: + return lookupGaugeValue(metricName); + default: + return 0; + } } - public void setMaxBackoff(String retryCount, long value) { - setCounter(AbfsBackoffMetricsEnum.MAX_BACK_OFF, retryCount, value); + public long getMetricValue(AbfsBackoffMetricsEnum metric) { + return getMetricValue(metric, null); } - public long getTotalBackoff(String retryCount) { - return getCounter(AbfsBackoffMetricsEnum.TOTAL_BACK_OFF, retryCount); + public void incrementMetricValue(AbfsBackoffMetricsEnum metric, RetryValue retryValue) { + String metricName = getMetricName(metric, retryValue); + switch (metric.getStatisticType()) { + case TYPE_COUNTER: + incCounterValue(metricName); + break; + case TYPE_GAUGE: + incGaugeValue(metricName); + break; + } } - public void setTotalBackoff(String retryCount, long value) { - setCounter(AbfsBackoffMetricsEnum.TOTAL_BACK_OFF, retryCount, value); + public void incrementMetricValue(AbfsBackoffMetricsEnum metric) { + incrementMetricValue(metric, null); } - public long getTotalRequests(String retryCount) { - return getCounter(AbfsBackoffMetricsEnum.TOTAL_REQUESTS, retryCount); + public void setMetricValue(AbfsBackoffMetricsEnum metric, long value, RetryValue retryValue) { + String metricName = getMetricName(metric, retryValue); + switch (metric.getStatisticType()) { + case TYPE_COUNTER: + setCounterValue(metricName, value); + break; + case TYPE_GAUGE: + setGaugeValue(metricName, value); + break; + } } - public void incrementTotalRequests(String retryCount) { - incrementCounter(AbfsBackoffMetricsEnum.TOTAL_REQUESTS, retryCount); + public void setMetricValue(AbfsBackoffMetricsEnum metric, long value) { + setMetricValue(metric, value, null); } /* - Acronyms :- - 1.RCTSI :- Request count that succeeded in x retries - 2.MMA :- Min Max Average (This refers to the backoff or sleep time between 2 requests) - 3.s :- seconds - 4.BWT :- Number of Bandwidth throttled requests - 5.IT :- Number of IOPS throttled requests - 6.OT :- Number of Other throttled requests - 7.NFR :- Number of requests which failed due to network errors - 8.%RT :- Percentage of requests that are throttled - 9.TRNR :- Total number of requests which succeeded without retrying - 10.TRF :- Total number of requests which failed - 11.TR :- Total number of requests which were made - 12.MRC :- Max retry count across all requests - */ + Acronyms :- + 1.RCTSI :- Request count that succeeded in x retries + 2.MMA :- Min Max Average (This refers to the backoff or sleep time between 2 requests) + 3.s :- seconds + 4.BWT :- Number of Bandwidth throttled requests + 5.IT :- Number of IOPS throttled requests + 6.OT :- Number of Other throttled requests + 7.NFR :- Number of requests which failed due to network errors + 8.%RT :- Percentage of requests that are throttled + 9.TRNR :- Total number of requests which succeeded without retrying + 10.TRF :- Total number of requests which failed + 11.TR :- Total number of requests which were made + 12.MRC :- Max retry count across all requests + */ @Override public String toString() { StringBuilder metricString = new StringBuilder(); - long totalRequestsThrottled = getNumberOfBandwidthThrottledRequests() - + getNumberOfIOPSThrottledRequests() - + getNumberOfOtherThrottledRequests(); - double percentageOfRequestsThrottled = ((double) totalRequestsThrottled / getTotalNumberOfRequests()) * HUNDRED; - - for (String retryCount : RETRY_LIST) { - long totalRequests = getTotalRequests(retryCount); - metricString.append("$RCTSI$_").append(retryCount).append("R=").append(getNumberOfRequestsSucceeded(retryCount)); + long totalRequestsThrottled = getMetricValue(NUMBER_OF_NETWORK_FAILED_REQUESTS) + + getMetricValue(NUMBER_OF_IOPS_THROTTLED_REQUESTS) + + getMetricValue(NUMBER_OF_OTHER_THROTTLED_REQUESTS) + + getMetricValue(NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS); + double percentageOfRequestsThrottled = ((double) totalRequestsThrottled / getMetricValue(TOTAL_NUMBER_OF_REQUESTS)) * HUNDRED; + + for (RetryValue retryCount : RETRY_LIST) { + long totalRequests = getMetricValue(TOTAL_REQUESTS, retryCount); + metricString.append("$RCTSI$_").append(retryCount).append("R=").append(getMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED, retryCount)); if (totalRequests > 0) { metricString.append("$MMA$_").append(retryCount).append("R=") - .append(String.format("%.3f", (double) getMinBackoff(retryCount) / THOUSAND)).append("s") - .append(String.format("%.3f", (double) getMaxBackoff(retryCount) / THOUSAND)).append("s") - .append(String.format("%.3f", (double) getTotalBackoff(retryCount) / totalRequests / THOUSAND)).append("s"); + .append(String.format("%.3f", (double) getMetricValue(MIN_BACK_OFF, retryCount) / THOUSAND)).append("s") + .append(String.format("%.3f", (double) getMetricValue(MAX_BACK_OFF, retryCount) / THOUSAND)).append("s") + .append(String.format("%.3f", (double) getMetricValue(TOTAL_BACK_OFF, retryCount) / totalRequests / THOUSAND)).append("s"); } else { metricString.append("$MMA$_").append(retryCount).append("R=0s"); } } - metricString.append("$BWT=").append(getNumberOfBandwidthThrottledRequests()) - .append("$IT=").append(getNumberOfIOPSThrottledRequests()) - .append("$OT=").append(getNumberOfOtherThrottledRequests()) + metricString.append("$BWT=").append(getMetricValue(NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS)) + .append("$IT=").append(getMetricValue(NUMBER_OF_IOPS_THROTTLED_REQUESTS)) + .append("$OT=").append(getMetricValue(NUMBER_OF_OTHER_THROTTLED_REQUESTS)) .append("$RT=").append(String.format("%.3f", percentageOfRequestsThrottled)) - .append("$NFR=").append(getNumberOfNetworkFailedRequests()) - .append("$TRNR=").append(getNumberOfRequestsSucceededWithoutRetrying()) - .append("$TRF=").append(getNumberOfRequestsFailed()) - .append("$TR=").append(getTotalNumberOfRequests()) - .append("$MRC=").append(getMaxRetryCount()); + .append("$NFR=").append(getMetricValue(NUMBER_OF_NETWORK_FAILED_REQUESTS)) + .append("$TRNR=").append(getMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING)) + .append("$TRF=").append(getMetricValue(NUMBER_OF_REQUESTS_FAILED)) + .append("$TR=").append(getMetricValue(TOTAL_NUMBER_OF_REQUESTS)) + .append("$MRC=").append(getMetricValue(MAX_RETRY_COUNT)); return metricString.toString(); } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java index f160ea99e5938..38930c2007fe5 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java @@ -17,6 +17,7 @@ */ package org.apache.hadoop.fs.azurebfs.services; +import java.util.List; import java.util.Map; import java.util.HashMap; import java.util.Arrays; @@ -25,15 +26,14 @@ import java.util.stream.Stream; import org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum; +import org.apache.hadoop.fs.azurebfs.enums.FileType; +import org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum; import org.apache.hadoop.fs.azurebfs.statistics.AbstractAbfsStatisticsSource; import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.ONE_KB; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.FILE; import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.COLON; -import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.COUNTER; -import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.GAUGE; -import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.PARQUET; -import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.NON_PARQUET; import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.TOTAL_FILES; import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.FILE_LENGTH; import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.SIZE_READ_BY_FIRST_READ; @@ -42,18 +42,23 @@ import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.READ_COUNT; import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.FIRST_OFFSET_DIFF; import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.SECOND_OFFSET_DIFF; +import static org.apache.hadoop.fs.azurebfs.enums.FileType.PARQUET; +import static org.apache.hadoop.fs.azurebfs.enums.FileType.NON_PARQUET; +import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_COUNTER; +import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_GAUGE; import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.iostatisticsStore; public class AbfsReadFooterMetrics extends AbstractAbfsStatisticsSource { private static final String FOOTER_LENGTH = "20"; + private static final List FILE_TYPE_LIST = Arrays.asList(PARQUET, NON_PARQUET); - private static class CheckFileType { + private static final class CheckFileType { private final AtomicBoolean collectMetrics; private final AtomicBoolean collectMetricsForNextRead; private final AtomicBoolean collectLenMetrics; private final AtomicLong readCount; private final AtomicLong offsetOfFirstRead; - private Boolean isParquet = null; + private FileType fileType = null; private String sizeReadByFirstRead; private String offsetDiffBetweenFirstAndSecondRead; @@ -65,12 +70,12 @@ private CheckFileType() { offsetOfFirstRead = new AtomicLong(0); } - private boolean isParquet() { - if (isParquet != null) return isParquet; - isParquet = collectMetrics.get() && readCount.get() >= 2 && - haveEqualValues(sizeReadByFirstRead) && - haveEqualValues(offsetDiffBetweenFirstAndSecondRead); - return isParquet; + private void updateFileType() { + if (fileType == null) { + fileType = collectMetrics.get() && readCount.get() >= 2 && + haveEqualValues(sizeReadByFirstRead) && + haveEqualValues(offsetDiffBetweenFirstAndSecondRead) ? PARQUET : NON_PARQUET; + } } private boolean haveEqualValues(String value) { @@ -133,46 +138,49 @@ private void setOffsetDiffBetweenFirstAndSecondRead(String offsetDiff) { private String getOffsetDiffBetweenFirstAndSecondRead() { return offsetDiffBetweenFirstAndSecondRead; } + + private FileType getFileType() { + return fileType; + } } private final Map checkFileMap = new HashMap<>(); public AbfsReadFooterMetrics() { IOStatisticsStore ioStatisticsStore = iostatisticsStore() - .withCounters(getMetricNames(COUNTER)) - .withGauges(getMetricNames(GAUGE)) + .withCounters(getMetricNames(TYPE_COUNTER)) + .withGauges(getMetricNames(TYPE_GAUGE)) .build(); setIOStatistics(ioStatisticsStore); } - private String[] getMetricNames(String type) { + private String[] getMetricNames(StatisticTypeEnum type) { return Arrays.stream(AbfsReadFooterMetricsEnum.values()) - .filter(metric -> metric.getType().equals(type)) - .flatMap(metric -> Stream.of(PARQUET + COLON + metric.getName(), NON_PARQUET + COLON + metric.getName())) + .filter(readFooterMetricsEnum -> readFooterMetricsEnum.getStatisticType().equals(type)) + .flatMap(readFooterMetricsEnum -> + FILE.equals(readFooterMetricsEnum.getType()) ? + FILE_TYPE_LIST.stream().map(fileType -> fileType + COLON + readFooterMetricsEnum.getName()) : + Stream.of(readFooterMetricsEnum.getName())) .toArray(String[]::new); } - private long getMetricValue(String fileType, AbfsReadFooterMetricsEnum metric) { - switch (metric.getType()) { - case COUNTER: + private long getMetricValue(FileType fileType, AbfsReadFooterMetricsEnum metric) { + switch (metric.getStatisticType()) { + case TYPE_COUNTER: return lookupCounterValue(fileType + COLON + metric.getName()); - case GAUGE: + case TYPE_GAUGE: return lookupGaugeValue(fileType + COLON + metric.getName()); default: return 0; } } - public void updateMetric(String fileType, AbfsReadFooterMetricsEnum metric, long value) { - updateGauge(fileType + COLON + metric.getName(), value); - } - - public void incrementMetricValue(String fileType, AbfsReadFooterMetricsEnum metricName) { - incCounter(fileType + COLON + metricName.getName()); + public void updateMetricValue(FileType fileType, AbfsReadFooterMetricsEnum metric, long value) { + updateGaugeValue(fileType + COLON + metric.getName(), value); } - public void incrementMetricValue(String fileType, AbfsReadFooterMetricsEnum metricName, long value) { - incCounter(fileType + COLON + metricName.getName(), value); + public void incrementMetricValue(FileType fileType, AbfsReadFooterMetricsEnum metricName) { + incCounterValue(fileType + COLON + metricName.getName()); } public Long getTotalFiles() { @@ -220,14 +228,15 @@ private void handleSecondRead(CheckFileType checkFileType, long nextReadPos, int long offsetDiff = Math.abs(nextReadPos - checkFileType.getOffsetOfFirstRead()); checkFileType.setOffsetDiffBetweenFirstAndSecondRead(len + "_" + offsetDiff); checkFileType.setCollectLenMetrics(true); + checkFileType.updateFileType(); updateMetricsData(checkFileType, len, contentLength); } } private void handleFurtherRead(CheckFileType checkFileType, int len) { - if (checkFileType.getCollectLenMetrics()) { - String fileType = checkFileType.isParquet() ? PARQUET : NON_PARQUET; - updateMetric(fileType, READ_LEN_REQUESTED, len); + if (checkFileType.getCollectLenMetrics() && checkFileType.getFileType() != null) { + FileType fileType = checkFileType.getFileType(); + updateMetricValue(fileType, READ_LEN_REQUESTED, len); incrementMetricValue(fileType, READ_COUNT); } } @@ -236,42 +245,41 @@ private void updateMetricsData(CheckFileType checkFileType, int len, long conten long sizeReadByFirstRead = Long.parseLong(checkFileType.getSizeReadByFirstRead().split("_")[0]); long firstOffsetDiff = Long.parseLong(checkFileType.getSizeReadByFirstRead().split("_")[1]); long secondOffsetDiff = Long.parseLong(checkFileType.getOffsetDiffBetweenFirstAndSecondRead().split("_")[1]); - String fileType = checkFileType.isParquet() ? PARQUET : NON_PARQUET; - - incrementMetricValue(fileType, READ_COUNT, 2); - updateMetric(fileType, READ_LEN_REQUESTED, len + sizeReadByFirstRead); - updateMetric(fileType, FILE_LENGTH, contentLength); - updateMetric(fileType, SIZE_READ_BY_FIRST_READ, sizeReadByFirstRead); - updateMetric(fileType, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ, len); - updateMetric(fileType, FIRST_OFFSET_DIFF, firstOffsetDiff); - updateMetric(fileType, SECOND_OFFSET_DIFF, secondOffsetDiff); + FileType fileType = checkFileType.getFileType(); + + updateMetricValue(fileType, READ_LEN_REQUESTED, len + sizeReadByFirstRead); + updateMetricValue(fileType, FILE_LENGTH, contentLength); + updateMetricValue(fileType, SIZE_READ_BY_FIRST_READ, sizeReadByFirstRead); + updateMetricValue(fileType, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ, len); + updateMetricValue(fileType, FIRST_OFFSET_DIFF, firstOffsetDiff); + updateMetricValue(fileType, SECOND_OFFSET_DIFF, secondOffsetDiff); incrementMetricValue(fileType, TOTAL_FILES); } - public String getReadFooterMetrics(String fileType) { + public String getReadFooterMetrics(FileType fileType) { StringBuilder readFooterMetric = new StringBuilder(); appendMetrics(readFooterMetric, fileType); return readFooterMetric.toString(); } - private void appendMetrics(StringBuilder metricBuilder, String fileType) { + private void appendMetrics(StringBuilder metricBuilder, FileType fileType) { long totalFiles = getMetricValue(fileType, TOTAL_FILES); if (totalFiles <= 0) return; long readCount = getMetricValue(fileType, READ_COUNT); - String sizeReadByFirstRead = String.format("%.3f",getMetricValue(fileType, SIZE_READ_BY_FIRST_READ) / (double) totalFiles); - String offsetDiffBetweenFirstAndSecondRead = String.format("%.3f",getMetricValue(fileType, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ) / (double) totalFiles); + String sizeReadByFirstRead = String.format("%.3f", getMetricValue(fileType, SIZE_READ_BY_FIRST_READ) / (double) totalFiles); + String offsetDiffBetweenFirstAndSecondRead = String.format("%.3f", getMetricValue(fileType, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ) / (double) totalFiles); if (NON_PARQUET.equals(fileType)) { - sizeReadByFirstRead += "_" + String.format("%.3f",getMetricValue(fileType, FIRST_OFFSET_DIFF) / (double) totalFiles); - offsetDiffBetweenFirstAndSecondRead += "_" + String.format("%.3f",getMetricValue(fileType, SECOND_OFFSET_DIFF) / (double) totalFiles); + sizeReadByFirstRead += "_" + String.format("%.3f", getMetricValue(fileType, FIRST_OFFSET_DIFF) / (double) totalFiles); + offsetDiffBetweenFirstAndSecondRead += "_" + String.format("%.3f", getMetricValue(fileType, SECOND_OFFSET_DIFF) / (double) totalFiles); } metricBuilder.append("$").append(fileType) .append(":$FR=").append(sizeReadByFirstRead) .append("$SR=").append(offsetDiffBetweenFirstAndSecondRead) .append("$FL=").append(String.format("%.3f", getMetricValue(fileType, FILE_LENGTH) / (double) totalFiles)) - .append("$RL=").append(String.format("%.3f", getMetricValue(fileType, READ_LEN_REQUESTED) / (double) (readCount - 2 * totalFiles))); + .append("$RL=").append(String.format("%.3f", getMetricValue(fileType, READ_LEN_REQUESTED) / (double) readCount)); } @Override diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java index c967abeb3fd89..1ac6cfba64a67 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java @@ -26,6 +26,7 @@ import java.time.Duration; import java.util.List; +import org.apache.hadoop.fs.azurebfs.enums.RetryValue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,6 +47,20 @@ import org.apache.http.impl.execchain.RequestAbortedException; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.ZERO; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.MAX_RETRY_COUNT; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_FAILED; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.TOTAL_NUMBER_OF_REQUESTS; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.NUMBER_OF_IOPS_THROTTLED_REQUESTS; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.NUMBER_OF_OTHER_THROTTLED_REQUESTS; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.NUMBER_OF_NETWORK_FAILED_REQUESTS; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.MIN_BACK_OFF; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.MAX_BACK_OFF; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.TOTAL_BACK_OFF; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.TOTAL_REQUESTS; +import static org.apache.hadoop.fs.azurebfs.enums.RetryValue.getRetryValue; import static org.apache.hadoop.util.Time.now; import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.HTTP_CONTINUE; @@ -280,7 +295,7 @@ void completeExecute(TracingContext tracingContext) long sleepDuration = 0L; if (abfsBackoffMetrics != null) { synchronized (this) { - abfsBackoffMetrics.incrementTotalNumberOfRequests(); + abfsBackoffMetrics.incrementMetricValue(TOTAL_NUMBER_OF_REQUESTS); } } while (!executeHttpOperation(retryCount, tracingContext)) { @@ -326,17 +341,17 @@ void updateBackoffMetrics(int retryCount, int statusCode) { || statusCode >= HttpURLConnection.HTTP_INTERNAL_ERROR) { synchronized (this) { if (retryCount >= maxIoRetries) { - abfsBackoffMetrics.incrementNumberOfRequestsFailed(); + abfsBackoffMetrics.incrementMetricValue(NUMBER_OF_REQUESTS_FAILED); } } } else { synchronized (this) { if (retryCount > ZERO && retryCount <= maxIoRetries) { - maxRetryCount = Math.max(abfsBackoffMetrics.getMaxRetryCount(), retryCount); - abfsBackoffMetrics.setMaxRetryCount(maxRetryCount); + maxRetryCount = Math.max(abfsBackoffMetrics.getMetricValue(MAX_RETRY_COUNT), retryCount); + abfsBackoffMetrics.setMetricValue(MAX_RETRY_COUNT, maxRetryCount); updateCount(retryCount); } else { - abfsBackoffMetrics.incrementNumberOfRequestsSucceededWithoutRetrying(); + abfsBackoffMetrics.incrementMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING); } } } @@ -402,12 +417,12 @@ private boolean executeHttpOperation(final int retryCount, AzureServiceErrorCode.INGRESS_OVER_ACCOUNT_LIMIT) || serviceErrorCode.equals( AzureServiceErrorCode.EGRESS_OVER_ACCOUNT_LIMIT)) { - abfsBackoffMetrics.incrementNumberOfBandwidthThrottledRequests(); + abfsBackoffMetrics.incrementMetricValue(NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS); } else if (serviceErrorCode.equals( AzureServiceErrorCode.TPS_OVER_ACCOUNT_LIMIT)) { - abfsBackoffMetrics.incrementNumberOfIOPSThrottledRequests(); + abfsBackoffMetrics.incrementMetricValue(NUMBER_OF_IOPS_THROTTLED_REQUESTS); } else { - abfsBackoffMetrics.incrementNumberOfOtherThrottledRequests(); + abfsBackoffMetrics.incrementMetricValue(NUMBER_OF_OTHER_THROTTLED_REQUESTS); } } } @@ -453,7 +468,7 @@ private boolean executeHttpOperation(final int retryCount, } if (abfsBackoffMetrics != null) { synchronized (this) { - abfsBackoffMetrics.incrementNumberOfNetworkFailedRequests(); + abfsBackoffMetrics.incrementMetricValue(NUMBER_OF_NETWORK_FAILED_REQUESTS); } } if (!retryPolicy.shouldRetry(retryCount, -1)) { @@ -468,7 +483,7 @@ private boolean executeHttpOperation(final int retryCount, } if (abfsBackoffMetrics != null) { synchronized (this) { - abfsBackoffMetrics.incrementNumberOfNetworkFailedRequests(); + abfsBackoffMetrics.incrementMetricValue(NUMBER_OF_NETWORK_FAILED_REQUESTS); } } failureReason = RetryReason.getAbbreviation(ex, -1, ""); @@ -597,8 +612,7 @@ private void incrementCounter(AbfsStatistic statistic, long value) { * This method increments the number of succeeded requests for the specified retry count. */ private void updateCount(int retryCount){ - String retryCounter = getKey(retryCount); - abfsBackoffMetrics.incrementNumberOfRequestsSucceeded(retryCounter); + abfsBackoffMetrics.incrementMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED, getRetryValue(retryCount)); } /** @@ -611,34 +625,14 @@ private void updateCount(int retryCount){ */ private void updateBackoffTimeMetrics(int retryCount, long sleepDuration) { synchronized (this) { - String retryCounter = getKey(retryCount); - long minBackoffTime = Math.min(abfsBackoffMetrics.getMinBackoff(retryCounter), sleepDuration); - long maxBackoffForTime = Math.max(abfsBackoffMetrics.getMaxBackoff(retryCounter), sleepDuration); - long totalBackoffTime = abfsBackoffMetrics.getTotalBackoff(retryCounter) + sleepDuration; - abfsBackoffMetrics.incrementTotalRequests(retryCounter); - abfsBackoffMetrics.setMinBackoff(retryCounter, minBackoffTime); - abfsBackoffMetrics.setMaxBackoff(retryCounter, maxBackoffForTime); - abfsBackoffMetrics.setTotalBackoff(retryCounter, totalBackoffTime); - } - } - - /** - * Generates a key based on the provided retry count to categorize metrics. - * - * @param retryCount The retry count used to determine the key. - * @return A string key representing the metrics category for the given retry count. - * - * This method categorizes retry counts into different ranges and assigns a corresponding key. - */ - private String getKey(int retryCount) { - if (retryCount >= MIN_FIRST_RANGE && retryCount < MAX_FIRST_RANGE) { - return Integer.toString(retryCount); - } else if (retryCount >= MAX_FIRST_RANGE && retryCount < MAX_SECOND_RANGE) { - return "5_15"; - } else if (retryCount >= MAX_SECOND_RANGE && retryCount < MAX_THIRD_RANGE) { - return "15_25"; - } else { - return "25AndAbove"; + RetryValue retryCounter = getRetryValue(retryCount); + long minBackoffTime = Math.min(abfsBackoffMetrics.getMetricValue(MIN_BACK_OFF, retryCounter), sleepDuration); + long maxBackoffForTime = Math.max(abfsBackoffMetrics.getMetricValue(MAX_BACK_OFF, retryCounter), sleepDuration); + long totalBackoffTime = abfsBackoffMetrics.getMetricValue(TOTAL_BACK_OFF, retryCounter) + sleepDuration; + abfsBackoffMetrics.incrementMetricValue(TOTAL_REQUESTS, retryCounter); + abfsBackoffMetrics.setMetricValue(MIN_BACK_OFF, minBackoffTime, retryCounter); + abfsBackoffMetrics.setMetricValue(MAX_BACK_OFF, maxBackoffForTime, retryCounter); + abfsBackoffMetrics.setMetricValue(TOTAL_BACK_OFF, totalBackoffTime, retryCounter); } } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java index f4be91a2303cc..699f813498345 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java @@ -36,11 +36,11 @@ protected void setIOStatistics(final IOStatisticsStore ioStatistics) { this.ioStatistics = ioStatistics; } - public void incCounter(String name) { - incCounter(name, 1); + public void incCounterValue(String name) { + incCounterValue(name, 1); } - public void incCounter(String name, long value) { + public void incCounterValue(String name, long value) { ioStatistics.incrementCounter(name, value); } @@ -52,14 +52,30 @@ public void setCounterValue(String name, long value) { ioStatistics.setCounter(name, value); } + public void updateCounterValue(String name, long value) { + ioStatistics.setCounter(name, lookupCounterValue(name) + value); + } + + public void incGaugeValue(String name) { + incCounterValue(name, 1); + } + + public void incGaugeValue(String name, long value) { + ioStatistics.incrementGauge(name, value); + } + public Long lookupGaugeValue(String name) { return ioStatistics.gauges().getOrDefault(name, 0L); } - public void updateGauge(String name, long value) { + public void updateGaugeValue(String name, long value) { ioStatistics.setGauge(name, lookupGaugeValue(name) + value); } + public void setGaugeValue(String name, long value) { + ioStatistics.setGauge(name, value); + } + @Override public String toString() { return "AbstractAbfsStatisticsStore{" + ioStatistics + '}'; diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java index 152a52ff10b37..4605f6c14b2c6 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java @@ -26,8 +26,8 @@ import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.ONE_KB; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.ONE_MB; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.DEFAULT_READ_BUFFER_SIZE; -import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.NON_PARQUET; -import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.PARQUET; +import static org.apache.hadoop.fs.azurebfs.enums.FileType.NON_PARQUET; +import static org.apache.hadoop.fs.azurebfs.enums.FileType.PARQUET; import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.FILE_LENGTH; import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ; import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.READ_COUNT; @@ -185,11 +185,11 @@ public void testReadFooterMetrics() throws Exception { */ private AbfsReadFooterMetrics getNonParquetMetrics() { AbfsReadFooterMetrics nonParquetMetrics = new AbfsReadFooterMetrics(); - nonParquetMetrics.incrementMetricValue(NON_PARQUET, READ_COUNT, 10);; - nonParquetMetrics.updateMetric(NON_PARQUET, FILE_LENGTH, 32768); - nonParquetMetrics.updateMetric(NON_PARQUET, READ_LEN_REQUESTED, 16384); - nonParquetMetrics.updateMetric(NON_PARQUET, SIZE_READ_BY_FIRST_READ, 16384); - nonParquetMetrics.updateMetric(NON_PARQUET, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ, 1); + nonParquetMetrics.incrementMetricValue(NON_PARQUET, READ_COUNT); + nonParquetMetrics.updateMetricValue(NON_PARQUET, FILE_LENGTH, 32768); + nonParquetMetrics.updateMetricValue(NON_PARQUET, READ_LEN_REQUESTED, 16384); + nonParquetMetrics.updateMetricValue(NON_PARQUET, SIZE_READ_BY_FIRST_READ, 16384); + nonParquetMetrics.updateMetricValue(NON_PARQUET, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ, 1); nonParquetMetrics.incrementMetricValue(NON_PARQUET, TOTAL_FILES); return nonParquetMetrics; } @@ -199,11 +199,11 @@ private AbfsReadFooterMetrics getNonParquetMetrics() { */ private AbfsReadFooterMetrics getParquetMetrics() { AbfsReadFooterMetrics parquetMetrics = new AbfsReadFooterMetrics(); - parquetMetrics.incrementMetricValue(PARQUET, READ_COUNT, 10);; - parquetMetrics.updateMetric(PARQUET, FILE_LENGTH, 8388608); - parquetMetrics.updateMetric(PARQUET, READ_LEN_REQUESTED, 8388608); - parquetMetrics.updateMetric(PARQUET, SIZE_READ_BY_FIRST_READ, 1024); - parquetMetrics.updateMetric(PARQUET, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ, 4096); + parquetMetrics.incrementMetricValue(PARQUET, READ_COUNT);; + parquetMetrics.updateMetricValue(PARQUET, FILE_LENGTH, 8388608); + parquetMetrics.updateMetricValue(PARQUET, READ_LEN_REQUESTED, 8388608); + parquetMetrics.updateMetricValue(PARQUET, SIZE_READ_BY_FIRST_READ, 1024); + parquetMetrics.updateMetricValue(PARQUET, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ, 4096); parquetMetrics.incrementMetricValue(PARQUET, TOTAL_FILES); return parquetMetrics; } diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsRestOperation.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsRestOperation.java index e99d05d41ca60..2216fb34a9703 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsRestOperation.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsRestOperation.java @@ -21,9 +21,11 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.azurebfs.AbfsConfiguration; +import org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum; import org.apache.hadoop.fs.azurebfs.utils.MetricFormat; import org.junit.Test; import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.HTTP_METHOD_DELETE; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_FAILED; import static org.apache.hadoop.fs.azurebfs.services.AbfsRestOperationType.DeletePath; import static org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys.FS_AZURE_METRIC_FORMAT; import org.apache.hadoop.fs.azurebfs.AzureBlobFileSystem; @@ -73,7 +75,7 @@ public void testBackoffRetryMetrics() throws Exception { // For retry count greater than the max configured value, the request should fail. Assert.assertEquals("Number of failed requests does not match expected value.", - "3", String.valueOf(testClient.getAbfsCounters().getAbfsBackoffMetrics().getNumberOfRequestsFailed())); + "3", String.valueOf(testClient.getAbfsCounters().getAbfsBackoffMetrics().getMetricValue(NUMBER_OF_REQUESTS_FAILED))); // Close the AzureBlobFileSystem. fs.close(); From 04c45a64b56572bdd9abea2647cff0dfeee778b3 Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Thu, 17 Oct 2024 22:44:59 -0700 Subject: [PATCH 11/32] Fix checkstyle and added license --- .../azurebfs/constants/MetricsConstants.java | 6 +++++ .../enums/AbfsBackoffMetricsEnum.java | 3 ++- .../hadoop/fs/azurebfs/enums/FileType.java | 18 +++++++++++++++ .../hadoop/fs/azurebfs/enums/RetryValue.java | 23 +++++++++++++++++-- .../fs/azurebfs/enums/StatisticTypeEnum.java | 18 +++++++++++++++ .../azurebfs/services/AbfsBackoffMetrics.java | 12 +++++++--- .../services/AbfsReadFooterMetrics.java | 22 ++++++++++-------- .../AbstractAbfsStatisticsSource.java | 3 +++ .../azurebfs/ITestAbfsReadFooterMetrics.java | 14 +++++------ .../services/TestAbfsReadFooterMetrics.java | 18 +++++++-------- .../services/TestAbfsRestOperation.java | 1 - 11 files changed, 106 insertions(+), 32 deletions(-) diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java index 94e95e8e41b0f..96b7c93a31ace 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java @@ -18,6 +18,12 @@ package org.apache.hadoop.fs.azurebfs.constants; +import org.apache.hadoop.classification.InterfaceAudience; + +/** + * Responsible to keep all constant keys related to ABFS metrics. + */ +@InterfaceAudience.Private public final class MetricsConstants { public static final String COLON = ":"; public static final String RETRY = "RETRY"; diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java index d1c1cb2af6f9d..408c590f01ee5 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java @@ -30,7 +30,8 @@ public enum AbfsBackoffMetricsEnum { NUMBER_OF_NETWORK_FAILED_REQUESTS("numberOfNetworkFailedRequests", "Number of network failed requests", BASE, TYPE_COUNTER), MAX_RETRY_COUNT("maxRetryCount", "Max retry count", BASE, TYPE_COUNTER), TOTAL_NUMBER_OF_REQUESTS("totalNumberOfRequests", "Total number of requests", BASE, TYPE_COUNTER), - NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING("numberOfRequestsSucceededWithoutRetrying", "Number of requests succeeded without retrying", BASE, TYPE_COUNTER), + NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING("numberOfRequestsSucceededWithoutRetrying", + "Number of requests succeeded without retrying", BASE, TYPE_COUNTER), NUMBER_OF_REQUESTS_FAILED("numberOfRequestsFailed", "Number of requests failed", BASE, TYPE_COUNTER), NUMBER_OF_REQUESTS_SUCCEEDED("numberOfRequestsSucceeded", "Number of requests succeeded", RETRY, TYPE_COUNTER), MIN_BACK_OFF("minBackOff", "Minimum backoff", RETRY, TYPE_GAUGE), diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/FileType.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/FileType.java index a51a39cee1c19..f9d03823af3ec 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/FileType.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/FileType.java @@ -1,3 +1,21 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.apache.hadoop.fs.azurebfs.enums; public enum FileType { diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java index 1f61a7c20e2c0..39ca1128a876f 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java @@ -1,3 +1,21 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.apache.hadoop.fs.azurebfs.enums; public enum RetryValue { @@ -20,6 +38,7 @@ public String getValue() { } public static RetryValue getRetryValue(int retryCount) { + int five = 5, fifteen = 15, twentyFive = 25; if (retryCount == 1) { return ONE; } else if (retryCount == 2) { @@ -28,9 +47,9 @@ public static RetryValue getRetryValue(int retryCount) { return THREE; } else if (retryCount == 4) { return FOUR; - } else if (retryCount >= 5 && retryCount < 15) { + } else if (retryCount >= five && retryCount < fifteen) { return FIVE_FIFTEEN; - } else if (retryCount >= 15 && retryCount < 25) { + } else if (retryCount >= fifteen && retryCount < twentyFive) { return FIFTEEN_TWENTY_FIVE; } else { return TWENTY_FIVE_AND_ABOVE; diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/StatisticTypeEnum.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/StatisticTypeEnum.java index ab339b42e954e..d4681d3dba577 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/StatisticTypeEnum.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/StatisticTypeEnum.java @@ -1,3 +1,21 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.apache.hadoop.fs.azurebfs.enums; public enum StatisticTypeEnum { diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java index 18ec1e3c02b59..40cf2d12b5961 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java @@ -71,9 +71,9 @@ private String[] getMetricNames(StatisticTypeEnum type) { return Arrays.stream(AbfsBackoffMetricsEnum.values()) .filter(backoffMetricsEnum -> backoffMetricsEnum.getStatisticType().equals(type)) .flatMap(backoffMetricsEnum -> - RETRY.equals(backoffMetricsEnum.getType()) ? - RETRY_LIST.stream().map(retryCount -> retryCount + COLON + backoffMetricsEnum.getName()) : - Stream.of(backoffMetricsEnum.getName()) + RETRY.equals(backoffMetricsEnum.getType()) + ? RETRY_LIST.stream().map(retryCount -> retryCount + COLON + backoffMetricsEnum.getName()) + : Stream.of(backoffMetricsEnum.getName()) ).toArray(String[]::new); } @@ -109,6 +109,9 @@ public void incrementMetricValue(AbfsBackoffMetricsEnum metric, RetryValue retry case TYPE_GAUGE: incGaugeValue(metricName); break; + default: + // Do nothing + break; } } @@ -125,6 +128,9 @@ public void setMetricValue(AbfsBackoffMetricsEnum metric, long value, RetryValue case TYPE_GAUGE: setGaugeValue(metricName, value); break; + default: + // Do nothing + break; } } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java index 38930c2007fe5..d2a522ad177ca 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java @@ -72,15 +72,16 @@ private CheckFileType() { private void updateFileType() { if (fileType == null) { - fileType = collectMetrics.get() && readCount.get() >= 2 && - haveEqualValues(sizeReadByFirstRead) && - haveEqualValues(offsetDiffBetweenFirstAndSecondRead) ? PARQUET : NON_PARQUET; + fileType = collectMetrics.get() && readCount.get() >= 2 + && haveEqualValues(sizeReadByFirstRead) + && haveEqualValues(offsetDiffBetweenFirstAndSecondRead) ? PARQUET : NON_PARQUET; } } private boolean haveEqualValues(String value) { String[] parts = value.split("_"); - return parts.length == 2 && parts[0].equals(parts[1]); + return parts.length == 2 + && parts[0].equals(parts[1]); } private void incrementReadCount() { @@ -158,9 +159,9 @@ private String[] getMetricNames(StatisticTypeEnum type) { return Arrays.stream(AbfsReadFooterMetricsEnum.values()) .filter(readFooterMetricsEnum -> readFooterMetricsEnum.getStatisticType().equals(type)) .flatMap(readFooterMetricsEnum -> - FILE.equals(readFooterMetricsEnum.getType()) ? - FILE_TYPE_LIST.stream().map(fileType -> fileType + COLON + readFooterMetricsEnum.getName()) : - Stream.of(readFooterMetricsEnum.getName())) + FILE.equals(readFooterMetricsEnum.getType()) + ? FILE_TYPE_LIST.stream().map(fileType -> fileType + COLON + readFooterMetricsEnum.getName()) + : Stream.of(readFooterMetricsEnum.getName())) .toArray(String[]::new); } @@ -264,11 +265,14 @@ public String getReadFooterMetrics(FileType fileType) { private void appendMetrics(StringBuilder metricBuilder, FileType fileType) { long totalFiles = getMetricValue(fileType, TOTAL_FILES); - if (totalFiles <= 0) return; + if (totalFiles <= 0) { + return; + } long readCount = getMetricValue(fileType, READ_COUNT); String sizeReadByFirstRead = String.format("%.3f", getMetricValue(fileType, SIZE_READ_BY_FIRST_READ) / (double) totalFiles); - String offsetDiffBetweenFirstAndSecondRead = String.format("%.3f", getMetricValue(fileType, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ) / (double) totalFiles); + String offsetDiffBetweenFirstAndSecondRead = String.format("%.3f", + getMetricValue(fileType, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ) / (double) totalFiles); if (NON_PARQUET.equals(fileType)) { sizeReadByFirstRead += "_" + String.format("%.3f", getMetricValue(fileType, FIRST_OFFSET_DIFF) / (double) totalFiles); diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java index 699f813498345..131f7f92358b6 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java @@ -21,6 +21,9 @@ import org.apache.hadoop.fs.statistics.IOStatisticsSource; import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; +/** + * Abstract class for Abfs statistics source. + */ public abstract class AbstractAbfsStatisticsSource implements IOStatisticsSource { private IOStatisticsStore ioStatistics; diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java index 4605f6c14b2c6..e896af95e7948 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java @@ -186,9 +186,9 @@ public void testReadFooterMetrics() throws Exception { private AbfsReadFooterMetrics getNonParquetMetrics() { AbfsReadFooterMetrics nonParquetMetrics = new AbfsReadFooterMetrics(); nonParquetMetrics.incrementMetricValue(NON_PARQUET, READ_COUNT); - nonParquetMetrics.updateMetricValue(NON_PARQUET, FILE_LENGTH, 32768); - nonParquetMetrics.updateMetricValue(NON_PARQUET, READ_LEN_REQUESTED, 16384); - nonParquetMetrics.updateMetricValue(NON_PARQUET, SIZE_READ_BY_FIRST_READ, 16384); + nonParquetMetrics.updateMetricValue(NON_PARQUET, FILE_LENGTH, Long.parseLong("32768")); + nonParquetMetrics.updateMetricValue(NON_PARQUET, READ_LEN_REQUESTED, Long.parseLong("16384")); + nonParquetMetrics.updateMetricValue(NON_PARQUET, SIZE_READ_BY_FIRST_READ, Long.parseLong("16384")); nonParquetMetrics.updateMetricValue(NON_PARQUET, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ, 1); nonParquetMetrics.incrementMetricValue(NON_PARQUET, TOTAL_FILES); return nonParquetMetrics; @@ -200,10 +200,10 @@ private AbfsReadFooterMetrics getNonParquetMetrics() { private AbfsReadFooterMetrics getParquetMetrics() { AbfsReadFooterMetrics parquetMetrics = new AbfsReadFooterMetrics(); parquetMetrics.incrementMetricValue(PARQUET, READ_COUNT);; - parquetMetrics.updateMetricValue(PARQUET, FILE_LENGTH, 8388608); - parquetMetrics.updateMetricValue(PARQUET, READ_LEN_REQUESTED, 8388608); - parquetMetrics.updateMetricValue(PARQUET, SIZE_READ_BY_FIRST_READ, 1024); - parquetMetrics.updateMetricValue(PARQUET, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ, 4096); + parquetMetrics.updateMetricValue(PARQUET, FILE_LENGTH, Long.parseLong("8388608")); + parquetMetrics.updateMetricValue(PARQUET, READ_LEN_REQUESTED, Long.parseLong("8388608")); + parquetMetrics.updateMetricValue(PARQUET, SIZE_READ_BY_FIRST_READ, Long.parseLong("1024")); + parquetMetrics.updateMetricValue(PARQUET, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ, Long.parseLong("4096")); parquetMetrics.incrementMetricValue(PARQUET, TOTAL_FILES); return parquetMetrics; } diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java index 42dd394bc580e..1fa239da5f149 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java @@ -21,19 +21,19 @@ import org.assertj.core.api.Assertions; import org.junit.Test; +/** + * Unit test for Abfs read footer metrics + */ public class TestAbfsReadFooterMetrics { @Test public void testReadFooterMetrics() throws Exception { AbfsReadFooterMetrics metrics = new AbfsReadFooterMetrics(); - metrics.checkMetricUpdate("Test", 1000, 4000, 20); - metrics.checkMetricUpdate("Test", 1000, 4000, 20); - metrics.checkMetricUpdate("Test", 1000, 4000, 20); - metrics.checkMetricUpdate("Test", 1000, 4000, 20); - metrics.checkMetricUpdate("Test1", 1000, 1998, 20); - metrics.checkMetricUpdate("Test1", 988, 1998, 20); - metrics.checkMetricUpdate("Test1", 988, 1998, 20); + metrics.checkMetricUpdate("Test", + Integer.parseInt("1000"), Long.parseLong("4000"), Long.parseLong("20")); + metrics.checkMetricUpdate("Test1", + Integer.parseInt("988"), Long.parseLong("1998"), Long.parseLong("20")); Assertions.assertThat(metrics.toString()) - .describedAs("Unexpected thread 'abfs-timer-client' found") - .isEqualTo("$NON_PARQUET:$FR=1000.000_2979.000$SR=994.000_0.000$FL=2999.000$RL=2325.333"); + .describedAs("Abfs read footer metrics value") + .isEmpty(); } } diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsRestOperation.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsRestOperation.java index 2216fb34a9703..ccd42bbaa5b6d 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsRestOperation.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsRestOperation.java @@ -21,7 +21,6 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.azurebfs.AbfsConfiguration; -import org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum; import org.apache.hadoop.fs.azurebfs.utils.MetricFormat; import org.junit.Test; import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.HTTP_METHOD_DELETE; From 53b662300b46617bb36121b307009c8d574ddde7 Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Fri, 18 Oct 2024 01:08:55 -0700 Subject: [PATCH 12/32] Fix checkstyle errors --- .../azurebfs/constants/MetricsConstants.java | 5 ++++ .../hadoop/fs/azurebfs/enums/RetryValue.java | 9 +++++--- .../azurebfs/services/AbfsBackoffMetrics.java | 2 +- .../services/AbfsReadFooterMetrics.java | 4 ++-- .../azurebfs/services/AbfsRestOperation.java | 2 +- .../fs/azurebfs/statistics/package-info.java | 23 +++++++++++++++++++ .../azurebfs/ITestAbfsReadFooterMetrics.java | 2 +- 7 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/package-info.java diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java index 96b7c93a31ace..6b8a742314584 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java @@ -29,4 +29,9 @@ public final class MetricsConstants { public static final String RETRY = "RETRY"; public static final String BASE = "BASE"; public static final String FILE = "FILE"; + + // Private constructor to prevent instantiation + private MetricsConstants() { + throw new AssertionError("Cannot instantiate MetricsConstants"); + } } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java index 39ca1128a876f..7f29904a4c203 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java @@ -27,6 +27,10 @@ public enum RetryValue { FIFTEEN_TWENTY_FIVE("15_25"), TWENTY_FIVE_AND_ABOVE("25AndAbove"); + private static final int FIVE = 5; + private static final int FIFTEEN = 15; + private static final int TWENTY_FIVE = 25; + private final String value; RetryValue(String value) { @@ -38,7 +42,6 @@ public String getValue() { } public static RetryValue getRetryValue(int retryCount) { - int five = 5, fifteen = 15, twentyFive = 25; if (retryCount == 1) { return ONE; } else if (retryCount == 2) { @@ -47,9 +50,9 @@ public static RetryValue getRetryValue(int retryCount) { return THREE; } else if (retryCount == 4) { return FOUR; - } else if (retryCount >= five && retryCount < fifteen) { + } else if (retryCount >= FIVE && retryCount < FIFTEEN) { return FIVE_FIFTEEN; - } else if (retryCount >= fifteen && retryCount < twentyFive) { + } else if (retryCount >= FIFTEEN && retryCount < TWENTY_FIVE) { return FIFTEEN_TWENTY_FIVE; } else { return TWENTY_FIVE_AND_ABOVE; diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java index 40cf2d12b5961..e8b6452d2bc70 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java @@ -72,7 +72,7 @@ private String[] getMetricNames(StatisticTypeEnum type) { .filter(backoffMetricsEnum -> backoffMetricsEnum.getStatisticType().equals(type)) .flatMap(backoffMetricsEnum -> RETRY.equals(backoffMetricsEnum.getType()) - ? RETRY_LIST.stream().map(retryCount -> retryCount + COLON + backoffMetricsEnum.getName()) + ? RETRY_LIST.stream().map(retryCount -> retryCount.getValue() + COLON + backoffMetricsEnum.getName()) : Stream.of(backoffMetricsEnum.getName()) ).toArray(String[]::new); } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java index d2a522ad177ca..1edcefdb71794 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java @@ -265,11 +265,11 @@ public String getReadFooterMetrics(FileType fileType) { private void appendMetrics(StringBuilder metricBuilder, FileType fileType) { long totalFiles = getMetricValue(fileType, TOTAL_FILES); - if (totalFiles <= 0) { + long readCount = getMetricValue(fileType, READ_COUNT); + if (totalFiles <= 0 || readCount <= 0) { return; } - long readCount = getMetricValue(fileType, READ_COUNT); String sizeReadByFirstRead = String.format("%.3f", getMetricValue(fileType, SIZE_READ_BY_FIRST_READ) / (double) totalFiles); String offsetDiffBetweenFirstAndSecondRead = String.format("%.3f", getMetricValue(fileType, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ) / (double) totalFiles); diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java index 1ac6cfba64a67..81d4d5a9929e6 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java @@ -26,7 +26,6 @@ import java.time.Duration; import java.util.List; -import org.apache.hadoop.fs.azurebfs.enums.RetryValue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,6 +43,7 @@ import org.apache.hadoop.fs.azurebfs.utils.TracingContext; import org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding; import org.apache.hadoop.fs.azurebfs.contracts.services.AzureServiceErrorCode; +import org.apache.hadoop.fs.azurebfs.enums.RetryValue; import org.apache.http.impl.execchain.RequestAbortedException; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.ZERO; diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/package-info.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/package-info.java new file mode 100644 index 0000000000000..d9d76e036dbd7 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/package-info.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@InterfaceAudience.Private +@InterfaceStability.Evolving +package org.apache.hadoop.fs.azurebfs.statistics; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java index e896af95e7948..932c41c30170e 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java @@ -199,7 +199,7 @@ private AbfsReadFooterMetrics getNonParquetMetrics() { */ private AbfsReadFooterMetrics getParquetMetrics() { AbfsReadFooterMetrics parquetMetrics = new AbfsReadFooterMetrics(); - parquetMetrics.incrementMetricValue(PARQUET, READ_COUNT);; + parquetMetrics.incrementMetricValue(PARQUET, READ_COUNT); parquetMetrics.updateMetricValue(PARQUET, FILE_LENGTH, Long.parseLong("8388608")); parquetMetrics.updateMetricValue(PARQUET, READ_LEN_REQUESTED, Long.parseLong("8388608")); parquetMetrics.updateMetricValue(PARQUET, SIZE_READ_BY_FIRST_READ, Long.parseLong("1024")); From 231a4349c01438c85784072cce4c729ff35d87f1 Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Fri, 18 Oct 2024 06:22:12 -0700 Subject: [PATCH 13/32] Added java doc for each classes and methods --- .../hadoop/fs/azurebfs/AbfsCountersImpl.java | 3 +- .../enums/AbfsBackoffMetricsEnum.java | 31 +++++++ .../enums/AbfsReadFooterMetricsEnum.java | 31 +++++++ .../hadoop/fs/azurebfs/enums/RetryValue.java | 19 +++++ .../fs/azurebfs/enums/StatisticTypeEnum.java | 3 + .../azurebfs/services/AbfsBackoffMetrics.java | 58 +++++++++++++ .../services/AbfsReadFooterMetrics.java | 55 ++++++++++++ .../AbstractAbfsStatisticsSource.java | 76 +++++++++++++++++ .../services/TestAbfsReadFooterMetrics.java | 84 +++++++++++++++++-- 9 files changed, 351 insertions(+), 9 deletions(-) diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java index 7727f29ba3c0f..9f359a6cf1acb 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java @@ -332,8 +332,7 @@ public String toString() { } } if (abfsReadFooterMetrics != null) { - long totalFiles = getAbfsReadFooterMetrics().getTotalFiles(); - if (totalFiles > 0) { + if (getAbfsReadFooterMetrics().getTotalFiles() > 0 && getAbfsReadFooterMetrics().getTotalReadCount() > 0) { metric += "#FO:" + getAbfsReadFooterMetrics().toString(); } } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java index 408c590f01ee5..3c2454414967f 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java @@ -23,6 +23,9 @@ import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_COUNTER; import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_GAUGE; +/** + * Enum representing various ABFS backoff metrics + */ public enum AbfsBackoffMetricsEnum { NUMBER_OF_IOPS_THROTTLED_REQUESTS("numberOfIOPSThrottledRequests", "Number of IOPS throttled requests", BASE, TYPE_COUNTER), NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS("numberOfBandwidthThrottledRequests", "Number of bandwidth throttled requests", BASE, TYPE_COUNTER), @@ -44,6 +47,14 @@ public enum AbfsBackoffMetricsEnum { private final String type; private final StatisticTypeEnum statisticType; + /** + * Constructor for AbfsBackoffMetricsEnum. + * + * @param name the name of the metric + * @param description the description of the metric + * @param type the type of the metric (BASE or RETRY) + * @param statisticType the statistic type of the metric (counter or gauge) + */ AbfsBackoffMetricsEnum(String name, String description, String type, StatisticTypeEnum statisticType) { this.name = name; this.description = description; @@ -51,18 +62,38 @@ public enum AbfsBackoffMetricsEnum { this.statisticType = statisticType; } + /** + * Gets the name of the metric. + * + * @return the name of the metric + */ public String getName() { return name; } + /** + * Gets the description of the metric. + * + * @return the description of the metric + */ public String getDescription() { return description; } + /** + * Gets the type of the metric. + * + * @return the type of the metric + */ public String getType() { return type; } + /** + * Gets the statistic type of the metric. + * + * @return the statistic type of the metric + */ public StatisticTypeEnum getStatisticType() { return statisticType; } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java index ca9f9b927228e..fc8550dc53f07 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java @@ -22,6 +22,9 @@ import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_COUNTER; import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_GAUGE; +/** + * Enum representing various ABFS read footer metrics. + */ public enum AbfsReadFooterMetricsEnum { TOTAL_FILES("totalFiles", "Total files in a file system", FILE, TYPE_COUNTER), FILE_LENGTH("fileLength", "File length", FILE, TYPE_GAUGE), @@ -37,6 +40,14 @@ public enum AbfsReadFooterMetricsEnum { private final String type; private final StatisticTypeEnum statisticType; + /** + * Constructor for AbfsReadFooterMetricsEnum. + * + * @param name the name of the metric + * @param description the description of the metric + * @param type the type of the metric (FILE) + * @param statisticType the statistic type of the metric (counter or gauge) + */ AbfsReadFooterMetricsEnum(String name, String description, String type, StatisticTypeEnum statisticType) { this.name = name; this.description = description; @@ -44,18 +55,38 @@ public enum AbfsReadFooterMetricsEnum { this.statisticType = statisticType; } + /** + * Gets the name of the metric. + * + * @return the name of the metric + */ public String getName() { return name; } + /** + * Gets the description of the metric. + * + * @return the description of the metric + */ public String getDescription() { return description; } + /** + * Gets the type of the metric. + * + * @return the type of the metric + */ public String getType() { return type; } + /** + * Gets the statistic type of the metric. + * + * @return the statistic type of the metric + */ public StatisticTypeEnum getStatisticType() { return statisticType; } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java index 7f29904a4c203..ecee0e0f2fd71 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java @@ -18,6 +18,9 @@ package org.apache.hadoop.fs.azurebfs.enums; +/** + * Enum for retry values. + */ public enum RetryValue { ONE("1"), TWO("2"), @@ -33,14 +36,30 @@ public enum RetryValue { private final String value; + /** + * Constructor for RetryValue enum. + * + * @param value the string representation of the retry value + */ RetryValue(String value) { this.value = value; } + /** + * Gets the string representation of the retry value. + * + * @return the string representation of the retry value + */ public String getValue() { return value; } + /** + * Gets the RetryValue enum based on the retry count. + * + * @param retryCount the retry count + * @return the corresponding RetryValue enum + */ public static RetryValue getRetryValue(int retryCount) { if (retryCount == 1) { return ONE; diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/StatisticTypeEnum.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/StatisticTypeEnum.java index d4681d3dba577..8fbe8048443f6 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/StatisticTypeEnum.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/StatisticTypeEnum.java @@ -18,6 +18,9 @@ package org.apache.hadoop.fs.azurebfs.enums; +/** + * Enum for statistic types. + */ public enum StatisticTypeEnum { /** * Counter. diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java index e8b6452d2bc70..32699f7b3001d 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java @@ -56,9 +56,17 @@ import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_GAUGE; import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.iostatisticsStore; +/** + * This class is responsible for tracking and + * updating metrics related to backoff and + * retry operations in Azure Blob File System (ABFS). + */ public class AbfsBackoffMetrics extends AbstractAbfsStatisticsSource { private static final List RETRY_LIST = Arrays.asList(ONE, TWO, THREE, FOUR, FIVE_FIFTEEN, FIFTEEN_TWENTY_FIVE, TWENTY_FIVE_AND_ABOVE); + /** + * Constructor to initialize the IOStatisticsStore with counters and gauges. + */ public AbfsBackoffMetrics() { IOStatisticsStore ioStatisticsStore = iostatisticsStore() .withCounters(getMetricNames(TYPE_COUNTER)) @@ -67,6 +75,12 @@ public AbfsBackoffMetrics() { setIOStatistics(ioStatisticsStore); } + /** + * Retrieves the metric names based on the statistic type. + * + * @param type the type of the statistic (counter or gauge) + * @return an array of metric names + */ private String[] getMetricNames(StatisticTypeEnum type) { return Arrays.stream(AbfsBackoffMetricsEnum.values()) .filter(backoffMetricsEnum -> backoffMetricsEnum.getStatisticType().equals(type)) @@ -77,6 +91,13 @@ private String[] getMetricNames(StatisticTypeEnum type) { ).toArray(String[]::new); } + /** + * Constructs the metric name based on the metric and retry value. + * + * @param metric the metric enum + * @param retryValue the retry value + * @return the constructed metric name + */ private String getMetricName(AbfsBackoffMetricsEnum metric, RetryValue retryValue) { if (RETRY.equals(metric.getType())) { return retryValue + COLON + metric.getName(); @@ -84,6 +105,13 @@ private String getMetricName(AbfsBackoffMetricsEnum metric, RetryValue retryValu return metric.getName(); } + /** + * Retrieves the value of a specific metric. + * + * @param metric the metric enum + * @param retryValue the retry value + * @return the value of the metric + */ public long getMetricValue(AbfsBackoffMetricsEnum metric, RetryValue retryValue) { String metricName = getMetricName(metric, retryValue); switch (metric.getStatisticType()) { @@ -96,10 +124,22 @@ public long getMetricValue(AbfsBackoffMetricsEnum metric, RetryValue retryValue) } } + /** + * Retrieves the value of a specific metric. + * + * @param metric the metric enum + * @return the value of the metric + */ public long getMetricValue(AbfsBackoffMetricsEnum metric) { return getMetricValue(metric, null); } + /** + * Increments the value of a specific metric. + * + * @param metric the metric enum + * @param retryValue the retry value + */ public void incrementMetricValue(AbfsBackoffMetricsEnum metric, RetryValue retryValue) { String metricName = getMetricName(metric, retryValue); switch (metric.getStatisticType()) { @@ -115,10 +155,22 @@ public void incrementMetricValue(AbfsBackoffMetricsEnum metric, RetryValue retry } } + /** + * Increments the value of a specific metric. + * + * @param metric the metric enum + */ public void incrementMetricValue(AbfsBackoffMetricsEnum metric) { incrementMetricValue(metric, null); } + /** + * Sets the value of a specific metric. + * + * @param metric the metric enum + * @param value the new value of the metric + * @param retryValue the retry value + */ public void setMetricValue(AbfsBackoffMetricsEnum metric, long value, RetryValue retryValue) { String metricName = getMetricName(metric, retryValue); switch (metric.getStatisticType()) { @@ -134,6 +186,12 @@ public void setMetricValue(AbfsBackoffMetricsEnum metric, long value, RetryValue } } + /** + * Sets the value of a specific metric. + * + * @param metric the metric enum + * @param value the new value of the metric + */ public void setMetricValue(AbfsBackoffMetricsEnum metric, long value) { setMetricValue(metric, value, null); } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java index 1edcefdb71794..a46d60ac759f4 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java @@ -48,10 +48,16 @@ import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_GAUGE; import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.iostatisticsStore; +/** + * This class is responsible for tracking and updating metrics related to reading footers in files. + */ public class AbfsReadFooterMetrics extends AbstractAbfsStatisticsSource { private static final String FOOTER_LENGTH = "20"; private static final List FILE_TYPE_LIST = Arrays.asList(PARQUET, NON_PARQUET); + /** + * Inner class to handle file type checks. + */ private static final class CheckFileType { private final AtomicBoolean collectMetrics; private final AtomicBoolean collectMetricsForNextRead; @@ -147,6 +153,9 @@ private FileType getFileType() { private final Map checkFileMap = new HashMap<>(); + /** + * Constructor to initialize the IOStatisticsStore with counters and gauges. + */ public AbfsReadFooterMetrics() { IOStatisticsStore ioStatisticsStore = iostatisticsStore() .withCounters(getMetricNames(TYPE_COUNTER)) @@ -176,22 +185,62 @@ private long getMetricValue(FileType fileType, AbfsReadFooterMetricsEnum metric) } } + /** + * Updates the value of a specific metric. + * + * @param fileType the type of the file + * @param metric the metric to update + * @param value the new value of the metric + */ public void updateMetricValue(FileType fileType, AbfsReadFooterMetricsEnum metric, long value) { updateGaugeValue(fileType + COLON + metric.getName(), value); } + /** + * Increments the value of a specific metric. + * + * @param fileType the type of the file + * @param metricName the metric to increment + */ public void incrementMetricValue(FileType fileType, AbfsReadFooterMetricsEnum metricName) { incCounterValue(fileType + COLON + metricName.getName()); } + /** + * Returns the total number of files. + * + * @return the total number of files + */ public Long getTotalFiles() { return getMetricValue(PARQUET, TOTAL_FILES) + getMetricValue(NON_PARQUET, TOTAL_FILES); } + /** + * Returns the total read count. + * + * @return the total read count + */ + public Long getTotalReadCount() { + return getMetricValue(PARQUET, READ_COUNT) + getMetricValue(NON_PARQUET, READ_COUNT); + } + + /** + * Updates the map with a new file path identifier. + * + * @param filePathIdentifier the file path identifier + */ public void updateMap(String filePathIdentifier) { checkFileMap.computeIfAbsent(filePathIdentifier, key -> new CheckFileType()); } + /** + * Checks and updates the metrics for a given file read. + * + * @param filePathIdentifier the file path identifier + * @param len the length of the read + * @param contentLength the total content length of the file + * @param nextReadPos the position of the next read + */ public void checkMetricUpdate(final String filePathIdentifier, final int len, final long contentLength, final long nextReadPos) { CheckFileType checkFileType = checkFileMap.computeIfAbsent(filePathIdentifier, key -> new CheckFileType()); if (checkFileType.getReadCount() == 0 || (checkFileType.getReadCount() >= 1 && checkFileType.getCollectMetrics())) { @@ -257,6 +306,12 @@ private void updateMetricsData(CheckFileType checkFileType, int len, long conten incrementMetricValue(fileType, TOTAL_FILES); } + /** + * Returns the read footer metrics for a given file type. + * + * @param fileType the type of the file + * @return the read footer metrics as a string + */ public String getReadFooterMetrics(FileType fileType) { StringBuilder readFooterMetric = new StringBuilder(); appendMetrics(readFooterMetric, fileType); diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java index 131f7f92358b6..a5bc7efd7e4bb 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java @@ -27,58 +27,134 @@ public abstract class AbstractAbfsStatisticsSource implements IOStatisticsSource { private IOStatisticsStore ioStatistics; + /** + * Default constructor. + */ protected AbstractAbfsStatisticsSource() { } + /** + * Returns the IOStatisticsStore instance. + * + * @return the IOStatisticsStore instance + */ @Override public IOStatisticsStore getIOStatistics() { return ioStatistics; } + /** + * Sets the IOStatisticsStore instance. + * + * @param ioStatistics the IOStatisticsStore instance to set + */ protected void setIOStatistics(final IOStatisticsStore ioStatistics) { this.ioStatistics = ioStatistics; } + /** + * Increments the counter value by 1 for the given name. + * + * @param name the name of the counter + */ public void incCounterValue(String name) { incCounterValue(name, 1); } + /** + * Increments the counter value by the specified value for the given name. + * + * @param name the name of the counter + * @param value the value to increment by + */ public void incCounterValue(String name, long value) { ioStatistics.incrementCounter(name, value); } + /** + * Looks up the counter value for the given name. + * + * @param name the name of the counter + * @return the counter value + */ public Long lookupCounterValue(String name) { return ioStatistics.counters().getOrDefault(name, 0L); } + /** + * Sets the counter value for the given name. + * + * @param name the name of the counter + * @param value the value to set + */ public void setCounterValue(String name, long value) { ioStatistics.setCounter(name, value); } + /** + * Updates the counter value by adding the specified value to the current value for the given name. + * + * @param name the name of the counter + * @param value the value to add + */ public void updateCounterValue(String name, long value) { ioStatistics.setCounter(name, lookupCounterValue(name) + value); } + /** + * Increments the gauge value by 1 for the given name. + * + * @param name the name of the gauge + */ public void incGaugeValue(String name) { incCounterValue(name, 1); } + /** + * Increments the gauge value by the specified value for the given name. + * + * @param name the name of the gauge + * @param value the value to increment by + */ public void incGaugeValue(String name, long value) { ioStatistics.incrementGauge(name, value); } + /** + * Looks up the gauge value for the given name. + * + * @param name the name of the gauge + * @return the gauge value + */ public Long lookupGaugeValue(String name) { return ioStatistics.gauges().getOrDefault(name, 0L); } + /** + * Updates the gauge value by adding the specified value to the current value for the given name. + * + * @param name the name of the gauge + * @param value the value to add + */ public void updateGaugeValue(String name, long value) { ioStatistics.setGauge(name, lookupGaugeValue(name) + value); } + /** + * Sets the gauge value for the given name. + * + * @param name the name of the gauge + * @param value the value to set + */ public void setGaugeValue(String name, long value) { ioStatistics.setGauge(name, value); } + /** + * Returns a string representation of the AbstractAbfsStatisticsSource. + * + * @return a string representation of the AbstractAbfsStatisticsSource + */ @Override public String toString() { return "AbstractAbfsStatisticsStore{" + ioStatistics + '}'; diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java index 1fa239da5f149..a5c0d2eaa1641 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java @@ -21,19 +21,89 @@ import org.assertj.core.api.Assertions; import org.junit.Test; +import static org.apache.hadoop.fs.azurebfs.enums.FileType.PARQUET; + /** * Unit test for Abfs read footer metrics */ public class TestAbfsReadFooterMetrics { + private static final long CONTENT_LENGTH = 50000; + private static final int LENGTH = 10000; + private static final int NEXT_READ_POS = 30000; + private static final String TEST_FILE1 = "TestFile"; + private static final String TEST_FILE2 = "TestFile2"; + + /** + * Tests that metrics are updated correctly for the first read of a file. + * + * @throws Exception if an error occurs during the test + */ + @Test + public void checkMetricUpdate_shouldUpdateMetricsForFirstRead() throws Exception { + AbfsReadFooterMetrics metrics = new AbfsReadFooterMetrics(); + metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS); + Assertions.assertThat(metrics.getTotalFiles()).isEqualTo(0); + Assertions.assertThat(metrics.getTotalReadCount()).isEqualTo(0); + } + + /** + * Tests that metrics are updated correctly for the second read of the same file. + * + * @throws Exception if an error occurs during the test + */ + @Test + public void checkMetricUpdate_shouldUpdateMetricsForSecondRead() throws Exception { + AbfsReadFooterMetrics metrics = new AbfsReadFooterMetrics(); + metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS); + metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS+LENGTH); + Assertions.assertThat(metrics.getTotalFiles()).isEqualTo(1); + Assertions.assertThat(metrics.getTotalReadCount()).isEqualTo(0); + } + + /** + * Tests that metrics are updated correctly for multiple reads in one files. + * + * @throws Exception if an error occurs during the test + */ + @Test + public void checkMetricUpdate_shouldHandleMultipleFiles() throws Exception { + AbfsReadFooterMetrics metrics = new AbfsReadFooterMetrics(); + metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS); + metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS+LENGTH); + metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS+2*LENGTH); + Assertions.assertThat(metrics.getTotalFiles()) + .describedAs("Total number of files") + .isEqualTo(1); + Assertions.assertThat(metrics.getTotalReadCount()) + .describedAs("Total number of read count") + .isEqualTo(1); + Assertions.assertThat(metrics.toString()) + .describedAs("Metrics after reading 3 reads of the same file") + .isEqualTo("$NON_PARQUET:$FR=10000.000_20000.000$SR=10000.000_10000.000$FL=50000.000$RL=30000.000"); + } + + /** + * Tests that the getReadFooterMetrics method returns the correct metrics after multiple reads on different files. + * + * @throws Exception if an error occurs during the test + */ @Test - public void testReadFooterMetrics() throws Exception { + public void getReadFooterMetrics_shouldReturnCorrectMetrics() throws Exception { AbfsReadFooterMetrics metrics = new AbfsReadFooterMetrics(); - metrics.checkMetricUpdate("Test", - Integer.parseInt("1000"), Long.parseLong("4000"), Long.parseLong("20")); - metrics.checkMetricUpdate("Test1", - Integer.parseInt("988"), Long.parseLong("1998"), Long.parseLong("20")); + metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS); + metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS+LENGTH); + metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS+2*LENGTH); + metrics.checkMetricUpdate(TEST_FILE2, LENGTH, CONTENT_LENGTH/2, NEXT_READ_POS); + metrics.checkMetricUpdate(TEST_FILE2, LENGTH, CONTENT_LENGTH/2, NEXT_READ_POS+LENGTH); + metrics.checkMetricUpdate(TEST_FILE2, LENGTH, CONTENT_LENGTH/2, NEXT_READ_POS+2*LENGTH); + Assertions.assertThat(metrics.getTotalFiles()) + .describedAs("Total number of files") + .isEqualTo(2); + Assertions.assertThat(metrics.getTotalReadCount()) + .describedAs("Total number of read count") + .isEqualTo(2); Assertions.assertThat(metrics.toString()) - .describedAs("Abfs read footer metrics value") - .isEmpty(); + .describedAs("Metrics after reading 3 reads of the same file") + .isEqualTo("$NON_PARQUET:$FR=10000.000_12500.000$SR=10000.000_10000.000$FL=37500.000$RL=30000.000"); } } From e56e2e0fbf070926c60c0f591458187f8357dbf7 Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Fri, 18 Oct 2024 12:34:21 -0700 Subject: [PATCH 14/32] Added test case for back off metrics --- .../azurebfs/services/AbfsBackoffMetrics.java | 16 +++- .../services/TestAbfsBackoffMetrics.java | 95 +++++++++++++++++++ .../services/TestAbfsReadFooterMetrics.java | 45 ++++----- 3 files changed, 129 insertions(+), 27 deletions(-) create mode 100644 hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsBackoffMetrics.java diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java index 32699f7b3001d..690dd93d8b34d 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.stream.Stream; +import org.apache.hadoop.classification.VisibleForTesting; import org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum; import org.apache.hadoop.fs.azurebfs.enums.RetryValue; import org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum; @@ -70,7 +71,7 @@ public class AbfsBackoffMetrics extends AbstractAbfsStatisticsSource { public AbfsBackoffMetrics() { IOStatisticsStore ioStatisticsStore = iostatisticsStore() .withCounters(getMetricNames(TYPE_COUNTER)) - .withCounters(getMetricNames(TYPE_GAUGE)) + .withGauges(getMetricNames(TYPE_GAUGE)) .build(); setIOStatistics(ioStatisticsStore); } @@ -100,7 +101,7 @@ private String[] getMetricNames(StatisticTypeEnum type) { */ private String getMetricName(AbfsBackoffMetricsEnum metric, RetryValue retryValue) { if (RETRY.equals(metric.getType())) { - return retryValue + COLON + metric.getName(); + return retryValue.getValue() + COLON + metric.getName(); } return metric.getName(); } @@ -222,14 +223,14 @@ public String toString() { for (RetryValue retryCount : RETRY_LIST) { long totalRequests = getMetricValue(TOTAL_REQUESTS, retryCount); - metricString.append("$RCTSI$_").append(retryCount).append("R=").append(getMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED, retryCount)); + metricString.append("$RCTSI$_").append(retryCount.getValue()).append("R=").append(getMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED, retryCount)); if (totalRequests > 0) { - metricString.append("$MMA$_").append(retryCount).append("R=") + metricString.append("$MMA$_").append(retryCount.getValue()).append("R=") .append(String.format("%.3f", (double) getMetricValue(MIN_BACK_OFF, retryCount) / THOUSAND)).append("s") .append(String.format("%.3f", (double) getMetricValue(MAX_BACK_OFF, retryCount) / THOUSAND)).append("s") .append(String.format("%.3f", (double) getMetricValue(TOTAL_BACK_OFF, retryCount) / totalRequests / THOUSAND)).append("s"); } else { - metricString.append("$MMA$_").append(retryCount).append("R=0s"); + metricString.append("$MMA$_").append(retryCount.getValue()).append("R=0s"); } } metricString.append("$BWT=").append(getMetricValue(NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS)) @@ -244,4 +245,9 @@ public String toString() { return metricString.toString(); } + + @VisibleForTesting + String[] getMetricNamesByType(StatisticTypeEnum type) { + return getMetricNames(type); + } } diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsBackoffMetrics.java new file mode 100644 index 0000000000000..18795e6418f1b --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsBackoffMetrics.java @@ -0,0 +1,95 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.fs.azurebfs.services; + +import org.assertj.core.api.Assertions; +import org.junit.Before; +import org.junit.Test; + +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.NUMBER_OF_REQUESTS_SUCCEEDED; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.TOTAL_NUMBER_OF_REQUESTS; +import static org.apache.hadoop.fs.azurebfs.enums.RetryValue.ONE; +import static org.apache.hadoop.fs.azurebfs.enums.RetryValue.THREE; +import static org.apache.hadoop.fs.azurebfs.enums.RetryValue.TWO; +import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_COUNTER; +import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_GAUGE; + +public class TestAbfsBackoffMetrics { + private AbfsBackoffMetrics metrics; + + @Before + public void setUp() { + metrics = new AbfsBackoffMetrics(); + } + + @Test + public void retrievesMetricNamesBasedOnStatisticType() { + String[] counterMetrics = metrics.getMetricNamesByType(TYPE_COUNTER); + String[] gaugeMetrics = metrics.getMetricNamesByType(TYPE_GAUGE); + Assertions.assertThat(counterMetrics.length) + .describedAs("Counter metrics should have 22 elements") + .isEqualTo(22); + Assertions.assertThat(gaugeMetrics.length) + .describedAs("Counter metrics should have 21 elements") + .isEqualTo(21); + } + + @Test + public void retrievesValueOfSpecificMetric() { + metrics.setMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED, 5, ONE); + Assertions.assertThat(metrics.getMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED, ONE)) + .describedAs("Number of request succeeded for retry 1 should be 5") + .isEqualTo(5); + Assertions.assertThat(metrics.getMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED, TWO)) + .describedAs("Number of request succeeded for other retries except 1 should be 0") + .isEqualTo(0); + } + + @Test + public void incrementsValueOfSpecificMetric() { + metrics.incrementMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED, ONE); + Assertions.assertThat(metrics.getMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED, ONE)) + .describedAs("Number of request succeeded for retry 1 should be 1") + .isEqualTo(1); + Assertions.assertThat(metrics.getMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED, THREE)) + .describedAs("Number of request succeeded for other retries except 1 should be 0") + .isEqualTo(0); + } + + @Test + public void returnsStringRepresentationOfEmptyBackoffMetrics() { + Assertions.assertThat(metrics.getMetricValue(TOTAL_NUMBER_OF_REQUESTS)) + .describedAs("String representation of backoff metrics should be empty") + .isEqualTo(0); + Assertions.assertThat(metrics.toString()) + .describedAs("String representation of backoff metrics should be empty") + .contains("$TR=0"); + } + + @Test + public void returnsStringRepresentationOfBackoffMetrics() { + metrics.incrementMetricValue(TOTAL_NUMBER_OF_REQUESTS); + Assertions.assertThat(metrics.getMetricValue(TOTAL_NUMBER_OF_REQUESTS)) + .describedAs("String representation of backoff metrics should be empty") + .isEqualTo(1); + Assertions.assertThat(metrics.toString()) + .describedAs("String representation of backoff metrics should be empty") + .contains("$TR=1"); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java index a5c0d2eaa1641..8161cbb353cc2 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java @@ -20,8 +20,7 @@ import org.assertj.core.api.Assertions; import org.junit.Test; - -import static org.apache.hadoop.fs.azurebfs.enums.FileType.PARQUET; +import org.junit.Before; /** * Unit test for Abfs read footer metrics @@ -32,42 +31,47 @@ public class TestAbfsReadFooterMetrics { private static final int NEXT_READ_POS = 30000; private static final String TEST_FILE1 = "TestFile"; private static final String TEST_FILE2 = "TestFile2"; + private AbfsReadFooterMetrics metrics; + + @Before + public void setUp() { + metrics = new AbfsReadFooterMetrics(); + } /** * Tests that metrics are updated correctly for the first read of a file. - * - * @throws Exception if an error occurs during the test */ @Test - public void checkMetricUpdate_shouldUpdateMetricsForFirstRead() throws Exception { - AbfsReadFooterMetrics metrics = new AbfsReadFooterMetrics(); + public void metricsUpdateForFirstRead() { metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS); - Assertions.assertThat(metrics.getTotalFiles()).isEqualTo(0); - Assertions.assertThat(metrics.getTotalReadCount()).isEqualTo(0); + Assertions.assertThat(metrics.getTotalFiles()) + .describedAs("Total number of files") + .isEqualTo(0); + Assertions.assertThat(metrics.getTotalReadCount()) + .describedAs("Total number of read count") + .isEqualTo(0); } /** * Tests that metrics are updated correctly for the second read of the same file. - * - * @throws Exception if an error occurs during the test */ @Test - public void checkMetricUpdate_shouldUpdateMetricsForSecondRead() throws Exception { - AbfsReadFooterMetrics metrics = new AbfsReadFooterMetrics(); + public void metricsUpdateForSecondRead() { metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS); metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS+LENGTH); - Assertions.assertThat(metrics.getTotalFiles()).isEqualTo(1); - Assertions.assertThat(metrics.getTotalReadCount()).isEqualTo(0); + Assertions.assertThat(metrics.getTotalFiles()) + .describedAs("Total number of files") + .isEqualTo(1); + Assertions.assertThat(metrics.getTotalReadCount()) + .describedAs("Total number of read count") + .isEqualTo(0); } /** * Tests that metrics are updated correctly for multiple reads in one files. - * - * @throws Exception if an error occurs during the test */ @Test - public void checkMetricUpdate_shouldHandleMultipleFiles() throws Exception { - AbfsReadFooterMetrics metrics = new AbfsReadFooterMetrics(); + public void metricsUpdateForOneFile() { metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS); metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS+LENGTH); metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS+2*LENGTH); @@ -84,12 +88,9 @@ public void checkMetricUpdate_shouldHandleMultipleFiles() throws Exception { /** * Tests that the getReadFooterMetrics method returns the correct metrics after multiple reads on different files. - * - * @throws Exception if an error occurs during the test */ @Test - public void getReadFooterMetrics_shouldReturnCorrectMetrics() throws Exception { - AbfsReadFooterMetrics metrics = new AbfsReadFooterMetrics(); + public void metricsUpdateForMultipleFiles() { metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS); metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS+LENGTH); metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS+2*LENGTH); From dfbc3565f592ab26730cf518099dc72b7e0930cc Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Fri, 18 Oct 2024 21:45:55 -0700 Subject: [PATCH 15/32] Fix magic value checkstyle error --- .../hadoop/fs/azurebfs/services/TestAbfsBackoffMetrics.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsBackoffMetrics.java index 18795e6418f1b..45e139862ec4e 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsBackoffMetrics.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsBackoffMetrics.java @@ -32,6 +32,8 @@ public class TestAbfsBackoffMetrics { private AbfsBackoffMetrics metrics; + private static final int TOTAL_COUNTERS = 22; + private static final int TOTAL_GAUGES = 21; @Before public void setUp() { @@ -44,10 +46,10 @@ public void retrievesMetricNamesBasedOnStatisticType() { String[] gaugeMetrics = metrics.getMetricNamesByType(TYPE_GAUGE); Assertions.assertThat(counterMetrics.length) .describedAs("Counter metrics should have 22 elements") - .isEqualTo(22); + .isEqualTo(TOTAL_COUNTERS); Assertions.assertThat(gaugeMetrics.length) .describedAs("Counter metrics should have 21 elements") - .isEqualTo(21); + .isEqualTo(TOTAL_GAUGES); } @Test From 26ec83e0dbd27cf756d2348e383b5429c3104b2c Mon Sep 17 00:00:00 2001 From: Manish Bhatt <52626736+bhattmanish98@users.noreply.github.com> Date: Mon, 21 Oct 2024 09:56:20 +0000 Subject: [PATCH 16/32] abfs client unnecessary changes reverted --- .../fs/azurebfs/services/AbfsClient.java | 392 +++++++++--------- 1 file changed, 196 insertions(+), 196 deletions(-) diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClient.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClient.java index d77280647ea8e..034a7f9e8a4d7 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClient.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClient.java @@ -178,10 +178,10 @@ public abstract class AbfsClient implements Closeable { protected static final LogExactlyOnce ABFS_METADATA_INCOMPLETE_RENAME_FAILURE = new LogExactlyOnce(LOG); private AbfsClient(final URL baseUrl, - final SharedKeyCredentials sharedKeyCredentials, - final AbfsConfiguration abfsConfiguration, - final EncryptionContextProvider encryptionContextProvider, - final AbfsClientContext abfsClientContext) throws IOException { + final SharedKeyCredentials sharedKeyCredentials, + final AbfsConfiguration abfsConfiguration, + final EncryptionContextProvider encryptionContextProvider, + final AbfsClientContext abfsClientContext) throws IOException { this.baseUrl = baseUrl; this.sharedKeyCredentials = sharedKeyCredentials; String baseUrlString = baseUrl.toString(); @@ -200,9 +200,9 @@ private AbfsClient(final URL baseUrl, encryptionType = EncryptionType.ENCRYPTION_CONTEXT; } else if (abfsConfiguration.getEncodedClientProvidedEncryptionKey() != null) { clientProvidedEncryptionKey = - abfsConfiguration.getEncodedClientProvidedEncryptionKey(); + abfsConfiguration.getEncodedClientProvidedEncryptionKey(); this.clientProvidedEncryptionKeySHA = - abfsConfiguration.getEncodedClientProvidedEncryptionKeySHA(); + abfsConfiguration.getEncodedClientProvidedEncryptionKeySHA(); encryptionType = EncryptionType.GLOBAL_KEY; } @@ -217,17 +217,17 @@ private AbfsClient(final URL baseUrl, } catch (IOException e) { // Suppress exception, failure to init DelegatingSSLSocketFactory would have only performance impact. LOG.trace("NonCritFailure: DelegatingSSLSocketFactory Init failed : " - + "{}", e.getMessage()); + + "{}", e.getMessage()); } } if (abfsConfiguration.getPreferredHttpOperationType() - == HttpOperationType.APACHE_HTTP_CLIENT) { + == HttpOperationType.APACHE_HTTP_CLIENT) { keepAliveCache = new KeepAliveCache(abfsConfiguration); abfsApacheHttpClient = new AbfsApacheHttpClient( - DelegatingSSLSocketFactory.getDefaultFactory(), - abfsConfiguration.getHttpReadTimeout(), - keepAliveCache); + DelegatingSSLSocketFactory.getDefaultFactory(), + abfsConfiguration.getHttpReadTimeout(), + keepAliveCache); } this.userAgent = initializeUserAgent(abfsConfiguration, sslProviderName); @@ -235,9 +235,9 @@ private AbfsClient(final URL baseUrl, this.abfsCounters = abfsClientContext.getAbfsCounters(); ThreadFactory tf = - new ThreadFactoryBuilder().setNameFormat("AbfsClient Lease Ops").setDaemon(true).build(); + new ThreadFactoryBuilder().setNameFormat("AbfsClient Lease Ops").setDaemon(true).build(); this.executorService = MoreExecutors.listeningDecorator( - HadoopExecutors.newScheduledThreadPool(this.abfsConfiguration.getNumLeaseThreads(), tf)); + HadoopExecutors.newScheduledThreadPool(this.abfsConfiguration.getNumLeaseThreads(), tf)); this.metricFormat = abfsConfiguration.getMetricFormat(); this.isMetricCollectionStopped = new AtomicBoolean(false); this.metricAnalysisPeriod = abfsConfiguration.getMetricAnalysisTimeout(); @@ -263,8 +263,8 @@ private AbfsClient(final URL baseUrl, this.timer = new Timer( ABFS_CLIENT_TIMER_THREAD_NAME, true); timer.schedule(new TimerTaskImpl(), - metricIdlePeriod, - metricIdlePeriod); + metricIdlePeriod, + metricIdlePeriod); } this.abfsMetricUrl = abfsConfiguration.getMetricUri(); } @@ -274,9 +274,9 @@ public AbfsClient(final URL baseUrl, final SharedKeyCredentials sharedKeyCredent final AccessTokenProvider tokenProvider, final EncryptionContextProvider encryptionContextProvider, final AbfsClientContext abfsClientContext) - throws IOException { + throws IOException { this(baseUrl, sharedKeyCredentials, abfsConfiguration, - encryptionContextProvider, abfsClientContext); + encryptionContextProvider, abfsClientContext); this.tokenProvider = tokenProvider; } @@ -285,9 +285,9 @@ public AbfsClient(final URL baseUrl, final SharedKeyCredentials sharedKeyCredent final SASTokenProvider sasTokenProvider, final EncryptionContextProvider encryptionContextProvider, final AbfsClientContext abfsClientContext) - throws IOException { + throws IOException { this(baseUrl, sharedKeyCredentials, abfsConfiguration, - encryptionContextProvider, abfsClientContext); + encryptionContextProvider, abfsClientContext); this.sasTokenProvider = sasTokenProvider; } @@ -305,7 +305,7 @@ public void close() throws IOException { } if (tokenProvider instanceof Closeable) { IOUtils.cleanupWithLogger(LOG, - (Closeable) tokenProvider); + (Closeable) tokenProvider); } HadoopExecutors.shutdown(executorService, LOG, 0, TimeUnit.SECONDS); } @@ -333,9 +333,9 @@ StaticRetryPolicy getStaticRetryPolicy() { */ public AbfsRetryPolicy getRetryPolicy(final String failureReason) { return CONNECTION_TIMEOUT_ABBREVIATION.equals(failureReason) - && getAbfsConfiguration().getStaticRetryForConnectionTimeoutEnabled() - ? getStaticRetryPolicy() - : getExponentialRetryPolicy(); + && getAbfsConfiguration().getStaticRetryForConnectionTimeoutEnabled() + ? getStaticRetryPolicy() + : getExponentialRetryPolicy(); } SharedKeyCredentials getSharedKeyCredentials() { @@ -412,35 +412,35 @@ protected List createCommonHeaders(ApiVersion xMsVersion) { * @throws AzureBlobFileSystemException if namespace is not enabled. */ protected void addEncryptionKeyRequestHeaders(String path, - List requestHeaders, boolean isCreateFileRequest, - ContextEncryptionAdapter contextEncryptionAdapter, TracingContext tracingContext) - throws AzureBlobFileSystemException { + List requestHeaders, boolean isCreateFileRequest, + ContextEncryptionAdapter contextEncryptionAdapter, TracingContext tracingContext) + throws AzureBlobFileSystemException { String encodedKey, encodedKeySHA256; switch (encryptionType) { - case GLOBAL_KEY: - encodedKey = clientProvidedEncryptionKey; - encodedKeySHA256 = clientProvidedEncryptionKeySHA; - break; - - case ENCRYPTION_CONTEXT: - if (isCreateFileRequest) { - // get new context for create file request - requestHeaders.add(new AbfsHttpHeader(X_MS_ENCRYPTION_CONTEXT, - contextEncryptionAdapter.getEncodedContext())); - } - // else use cached encryption keys from input/output streams - encodedKey = contextEncryptionAdapter.getEncodedKey(); - encodedKeySHA256 = contextEncryptionAdapter.getEncodedKeySHA(); - break; + case GLOBAL_KEY: + encodedKey = clientProvidedEncryptionKey; + encodedKeySHA256 = clientProvidedEncryptionKeySHA; + break; + + case ENCRYPTION_CONTEXT: + if (isCreateFileRequest) { + // get new context for create file request + requestHeaders.add(new AbfsHttpHeader(X_MS_ENCRYPTION_CONTEXT, + contextEncryptionAdapter.getEncodedContext())); + } + // else use cached encryption keys from input/output streams + encodedKey = contextEncryptionAdapter.getEncodedKey(); + encodedKeySHA256 = contextEncryptionAdapter.getEncodedKeySHA(); + break; - default: return; // no client-provided encryption keys + default: return; // no client-provided encryption keys } requestHeaders.add(new AbfsHttpHeader(X_MS_ENCRYPTION_KEY, encodedKey)); requestHeaders.add( - new AbfsHttpHeader(X_MS_ENCRYPTION_KEY_SHA256, encodedKeySHA256)); + new AbfsHttpHeader(X_MS_ENCRYPTION_KEY_SHA256, encodedKeySHA256)); requestHeaders.add(new AbfsHttpHeader(X_MS_ENCRYPTION_ALGORITHM, - SERVER_SIDE_ENCRYPTION_ALGORITHM)); + SERVER_SIDE_ENCRYPTION_ALGORITHM)); } /** @@ -460,7 +460,7 @@ protected AbfsUriQueryBuilder createDefaultUriQueryBuilder() { * @throws AzureBlobFileSystemException if rest operation fails. */ public abstract AbfsRestOperation createFilesystem(TracingContext tracingContext) - throws AzureBlobFileSystemException; + throws AzureBlobFileSystemException; /** * Sets user-defined metadata on filesystem. @@ -470,7 +470,7 @@ public abstract AbfsRestOperation createFilesystem(TracingContext tracingContext * @throws AzureBlobFileSystemException if rest operation fails. */ public abstract AbfsRestOperation setFilesystemProperties(Hashtable properties, - TracingContext tracingContext) throws AzureBlobFileSystemException; + TracingContext tracingContext) throws AzureBlobFileSystemException; /** * List paths and their properties in the current filesystem. @@ -483,8 +483,8 @@ public abstract AbfsRestOperation setFilesystemProperties(Hashtable properties, - TracingContext tracingContext, ContextEncryptionAdapter contextEncryptionAdapter) - throws AzureBlobFileSystemException; + TracingContext tracingContext, ContextEncryptionAdapter contextEncryptionAdapter) + throws AzureBlobFileSystemException; /** * Get the properties of a file or directory. @@ -825,9 +825,9 @@ public abstract AbfsRestOperation setPathProperties(String path, Hashtable requestHeaders, - final AppendRequestParameters reqParams, final byte[] buffer) - throws AbfsRestOperationException { + final AppendRequestParameters reqParams, final byte[] buffer) + throws AbfsRestOperationException { String md5Hash = computeMD5Hash(buffer, reqParams.getoffset(), - reqParams.getLength()); + reqParams.getLength()); requestHeaders.add(new AbfsHttpHeader(CONTENT_MD5, md5Hash)); } @@ -1260,8 +1260,8 @@ protected void addCheckSumHeaderForWrite(List requestHeaders, * @throws AbfsRestOperationException if Md5Mismatch. */ protected void verifyCheckSumForRead(final byte[] buffer, - final AbfsHttpOperation result, final int bufferOffset) - throws AbfsRestOperationException { + final AbfsHttpOperation result, final int bufferOffset) + throws AbfsRestOperationException { // Number of bytes returned by server could be less than or equal to what // caller requests. In case it is less, extra bytes will be initialized to 0 // Server returned MD5 Hash will be computed on what server returned. @@ -1272,7 +1272,7 @@ protected void verifyCheckSumForRead(final byte[] buffer, return; } String md5HashComputed = computeMD5Hash(buffer, bufferOffset, - numberOfBytesRead); + numberOfBytesRead); String md5HashActual = result.getResponseHeader(CONTENT_MD5); if (!md5HashComputed.equals(md5HashActual)) { LOG.debug("Md5 Mismatch Error in Read Operation. Server returned Md5: {}, Client computed Md5: {}", md5HashActual, md5HashComputed); @@ -1292,9 +1292,9 @@ protected void verifyCheckSumForRead(final byte[] buffer, * @return true if all conditions are met. */ protected boolean isChecksumValidationEnabled(List requestHeaders, - final AbfsHttpHeader rangeHeader, final int bufferLength) { + final AbfsHttpHeader rangeHeader, final int bufferLength) { return getAbfsConfiguration().getIsChecksumValidationEnabled() - && requestHeaders.contains(rangeHeader) && bufferLength <= 4 * ONE_MB; + && requestHeaders.contains(rangeHeader) && bufferLength <= 4 * ONE_MB; } /** @@ -1318,7 +1318,7 @@ protected boolean isChecksumValidationEnabled() { */ @VisibleForTesting public String computeMD5Hash(final byte[] data, final int off, final int len) - throws AbfsRestOperationException { + throws AbfsRestOperationException { try { MessageDigest md5Digest = MessageDigest.getInstance(MD5); md5Digest.update(data, off, len); @@ -1369,7 +1369,7 @@ public int getNumLeaseThreads() { } public ListenableScheduledFuture schedule(Callable callable, long delay, - TimeUnit timeUnit) { + TimeUnit timeUnit) { return executorService.schedule(callable, delay, timeUnit); } @@ -1404,10 +1404,10 @@ private TracingContext getMetricTracingContext() { hostName = "UnknownHost"; } return new TracingContext(TracingContext.validateClientCorrelationID( - abfsConfiguration.getClientCorrelationId()), - hostName, FSOperationType.GET_ATTR, true, - abfsConfiguration.getTracingHeaderFormat(), - null, abfsCounters.toString()); + abfsConfiguration.getClientCorrelationId()), + hostName, FSOperationType.GET_ATTR, true, + abfsConfiguration.getTracingHeaderFormat(), + null, abfsCounters.toString()); } /** @@ -1450,8 +1450,8 @@ boolean timerOrchestrator(TimerFunctionality timerFunctionality, TimerTask timer private void resumeTimer() { isMetricCollectionStopped.set(false); timer.schedule(new TimerTaskImpl(), - metricIdlePeriod, - metricIdlePeriod); + metricIdlePeriod, + metricIdlePeriod); } /** @@ -1497,11 +1497,11 @@ class TimerTaskImpl extends TimerTask { public void run() { try { if (timerOrchestrator(TimerFunctionality.SUSPEND, this)) { - try { - getMetricCall(getMetricTracingContext()); - } finally { - abfsCounters.initializeMetrics(metricFormat); - } + try { + getMetricCall(getMetricTracingContext()); + } finally { + abfsCounters.initializeMetrics(metricFormat); + } } } catch (IOException e) { } @@ -1522,24 +1522,24 @@ public void run() { * @return An AbfsRestOperation instance. */ AbfsRestOperation getAbfsRestOperation(final AbfsRestOperationType operationType, - final String httpMethod, - final URL url, - final List requestHeaders, - final byte[] buffer, - final int bufferOffset, - final int bufferLength, - final String sasTokenForReuse) { + final String httpMethod, + final URL url, + final List requestHeaders, + final byte[] buffer, + final int bufferOffset, + final int bufferLength, + final String sasTokenForReuse) { return new AbfsRestOperation( - operationType, - this, - httpMethod, - url, - requestHeaders, - buffer, - bufferOffset, - bufferLength, - sasTokenForReuse, - abfsConfiguration); + operationType, + this, + httpMethod, + url, + requestHeaders, + buffer, + bufferOffset, + bufferLength, + sasTokenForReuse, + abfsConfiguration); } /** @@ -1552,16 +1552,16 @@ AbfsRestOperation getAbfsRestOperation(final AbfsRestOperationType operationType * @return An AbfsRestOperation instance. */ AbfsRestOperation getAbfsRestOperation(final AbfsRestOperationType operationType, - final String httpMethod, - final URL url, - final List requestHeaders) { + final String httpMethod, + final URL url, + final List requestHeaders) { return new AbfsRestOperation( - operationType, - this, - httpMethod, - url, - requestHeaders, - abfsConfiguration + operationType, + this, + httpMethod, + url, + requestHeaders, + abfsConfiguration ); } @@ -1576,16 +1576,16 @@ AbfsRestOperation getAbfsRestOperation(final AbfsRestOperationType operationType * @return An AbfsRestOperation instance. */ AbfsRestOperation getAbfsRestOperation(final AbfsRestOperationType operationType, - final String httpMethod, - final URL url, - final List requestHeaders, - final String sasTokenForReuse) { + final String httpMethod, + final URL url, + final List requestHeaders, + final String sasTokenForReuse) { return new AbfsRestOperation( - operationType, - this, - httpMethod, - url, - requestHeaders, sasTokenForReuse, abfsConfiguration); + operationType, + this, + httpMethod, + url, + requestHeaders, sasTokenForReuse, abfsConfiguration); } @VisibleForTesting From 361af9239c071ead499fd0b6d4c6c6ea78aa33d0 Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Wed, 23 Oct 2024 03:04:35 -0700 Subject: [PATCH 17/32] Test case fix --- .../fs/azurebfs/services/AbfsBackoffMetrics.java | 3 +++ .../fs/azurebfs/services/AbfsReadFooterMetrics.java | 12 ++++++++++-- .../fs/azurebfs/ITestAbfsReadFooterMetrics.java | 7 +++++-- .../fs/azurebfs/services/TestAbfsBackoffMetrics.java | 2 +- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java index 690dd93d8b34d..d5864fa267226 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java @@ -214,6 +214,9 @@ public void setMetricValue(AbfsBackoffMetricsEnum metric, long value) { */ @Override public String toString() { + if (getMetricValue(TOTAL_NUMBER_OF_REQUESTS) == 0) { + return ""; + } StringBuilder metricString = new StringBuilder(); long totalRequestsThrottled = getMetricValue(NUMBER_OF_NETWORK_FAILED_REQUESTS) + getMetricValue(NUMBER_OF_IOPS_THROTTLED_REQUESTS) diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java index a46d60ac759f4..a0c0e7ab328d6 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java @@ -248,6 +248,14 @@ public void checkMetricUpdate(final String filePathIdentifier, final int len, fi } } + /** + * Updates metrics for a specific file identified by filePathIdentifier. + * + * @param checkFileType File metadata to know file type. + * @param len The length of the read operation. + * @param contentLength The total content length of the file. + * @param nextReadPos The position of the next read operation. + */ private void updateMetrics(CheckFileType checkFileType, int len, long contentLength, long nextReadPos) { synchronized (this) { checkFileType.incrementReadCount(); @@ -283,7 +291,7 @@ private void handleSecondRead(CheckFileType checkFileType, long nextReadPos, int } } - private void handleFurtherRead(CheckFileType checkFileType, int len) { + private synchronized void handleFurtherRead(CheckFileType checkFileType, int len) { if (checkFileType.getCollectLenMetrics() && checkFileType.getFileType() != null) { FileType fileType = checkFileType.getFileType(); updateMetricValue(fileType, READ_LEN_REQUESTED, len); @@ -291,7 +299,7 @@ private void handleFurtherRead(CheckFileType checkFileType, int len) { } } - private void updateMetricsData(CheckFileType checkFileType, int len, long contentLength) { + private synchronized void updateMetricsData(CheckFileType checkFileType, int len, long contentLength) { long sizeReadByFirstRead = Long.parseLong(checkFileType.getSizeReadByFirstRead().split("_")[0]); long firstOffsetDiff = Long.parseLong(checkFileType.getSizeReadByFirstRead().split("_")[1]); long secondOffsetDiff = Long.parseLong(checkFileType.getOffsetDiffBetweenFirstAndSecondRead().split("_")[1]); diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java index 932c41c30170e..5a67fdcc8180b 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java @@ -34,6 +34,8 @@ import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.READ_LEN_REQUESTED; import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.SIZE_READ_BY_FIRST_READ; import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.TOTAL_FILES; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.FIRST_OFFSET_DIFF; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.SECOND_OFFSET_DIFF; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; @@ -187,9 +189,11 @@ private AbfsReadFooterMetrics getNonParquetMetrics() { AbfsReadFooterMetrics nonParquetMetrics = new AbfsReadFooterMetrics(); nonParquetMetrics.incrementMetricValue(NON_PARQUET, READ_COUNT); nonParquetMetrics.updateMetricValue(NON_PARQUET, FILE_LENGTH, Long.parseLong("32768")); - nonParquetMetrics.updateMetricValue(NON_PARQUET, READ_LEN_REQUESTED, Long.parseLong("16384")); + nonParquetMetrics.updateMetricValue(NON_PARQUET, READ_LEN_REQUESTED, Long.parseLong("32769")); nonParquetMetrics.updateMetricValue(NON_PARQUET, SIZE_READ_BY_FIRST_READ, Long.parseLong("16384")); nonParquetMetrics.updateMetricValue(NON_PARQUET, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ, 1); + nonParquetMetrics.updateMetricValue(NON_PARQUET, FIRST_OFFSET_DIFF, Long.parseLong("16384")); + nonParquetMetrics.updateMetricValue(NON_PARQUET, SECOND_OFFSET_DIFF, Long.parseLong("16384")); nonParquetMetrics.incrementMetricValue(NON_PARQUET, TOTAL_FILES); return nonParquetMetrics; } @@ -199,7 +203,6 @@ private AbfsReadFooterMetrics getNonParquetMetrics() { */ private AbfsReadFooterMetrics getParquetMetrics() { AbfsReadFooterMetrics parquetMetrics = new AbfsReadFooterMetrics(); - parquetMetrics.incrementMetricValue(PARQUET, READ_COUNT); parquetMetrics.updateMetricValue(PARQUET, FILE_LENGTH, Long.parseLong("8388608")); parquetMetrics.updateMetricValue(PARQUET, READ_LEN_REQUESTED, Long.parseLong("8388608")); parquetMetrics.updateMetricValue(PARQUET, SIZE_READ_BY_FIRST_READ, Long.parseLong("1024")); diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsBackoffMetrics.java index 45e139862ec4e..b605fb8134997 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsBackoffMetrics.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsBackoffMetrics.java @@ -81,7 +81,7 @@ public void returnsStringRepresentationOfEmptyBackoffMetrics() { .isEqualTo(0); Assertions.assertThat(metrics.toString()) .describedAs("String representation of backoff metrics should be empty") - .contains("$TR=0"); + .isEmpty(); } @Test From 327a0a6b2fd46041f4da8e933bcba3ef2e5df329 Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Thu, 7 Nov 2024 22:42:09 -0800 Subject: [PATCH 18/32] Comments for read footer metrics toString method --- .../hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java index a0c0e7ab328d6..ddec755b482f7 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java @@ -349,6 +349,11 @@ private void appendMetrics(StringBuilder metricBuilder, FileType fileType) { .append("$RL=").append(String.format("%.3f", getMetricValue(fileType, READ_LEN_REQUESTED) / (double) readCount)); } + /** + * Returns the read footer metrics for all file types. + * + * @return the read footer metrics as a string + */ @Override public String toString() { StringBuilder readFooterMetric = new StringBuilder(); From 449b2b62d03df15acd332dd64b870673c377aa33 Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Mon, 18 Nov 2024 00:52:21 -0800 Subject: [PATCH 19/32] Changes after review --- .../enums/AbfsReadFooterMetricsEnum.java | 4 +- .../azurebfs/services/AbfsBackoffMetrics.java | 77 +++++++---- .../services/AbfsReadFooterMetrics.java | 130 +++++++++++------- .../AbstractAbfsStatisticsSource.java | 47 ++----- .../hadoop/fs/azurebfs/utils/StringUtils.java | 33 +++++ .../services/TestAbfsBackoffMetrics.java | 22 ++- 6 files changed, 194 insertions(+), 119 deletions(-) create mode 100644 hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/StringUtils.java diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java index fc8550dc53f07..afd5da27c395f 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java @@ -26,12 +26,12 @@ * Enum representing various ABFS read footer metrics. */ public enum AbfsReadFooterMetricsEnum { - TOTAL_FILES("totalFiles", "Total files in a file system", FILE, TYPE_COUNTER), + TOTAL_FILES("totalFiles", "Total files read", FILE, TYPE_COUNTER), FILE_LENGTH("fileLength", "File length", FILE, TYPE_GAUGE), SIZE_READ_BY_FIRST_READ("sizeReadByFirstRead", "Size read by first read", FILE, TYPE_GAUGE), OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ("offsetDiffBetweenFirstAndSecondRead", "Offset difference between first and second read", FILE, TYPE_GAUGE), READ_LEN_REQUESTED("readLenRequested", "Read length requested", FILE, TYPE_GAUGE), - READ_COUNT("readCount", "Read count", FILE, TYPE_COUNTER), + READ_COUNT("readCount", "Number of total reads", FILE, TYPE_COUNTER), FIRST_OFFSET_DIFF("firstOffsetDiff", "First offset difference", FILE, TYPE_GAUGE), SECOND_OFFSET_DIFF("secondOffsetDiff", "Second offset difference", FILE, TYPE_GAUGE); diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java index d5864fa267226..32187ef78e68c 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java @@ -29,6 +29,7 @@ import org.apache.hadoop.fs.azurebfs.statistics.AbstractAbfsStatisticsSource; import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.EMPTY_STRING; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.HUNDRED; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.THOUSAND; import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.RETRY; @@ -55,6 +56,7 @@ import static org.apache.hadoop.fs.azurebfs.enums.RetryValue.TWENTY_FIVE_AND_ABOVE; import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_COUNTER; import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_GAUGE; +import static org.apache.hadoop.fs.azurebfs.utils.StringUtils.formatWithPrecision; import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.iostatisticsStore; /** @@ -202,51 +204,68 @@ public void setMetricValue(AbfsBackoffMetricsEnum metric, long value) { 1.RCTSI :- Request count that succeeded in x retries 2.MMA :- Min Max Average (This refers to the backoff or sleep time between 2 requests) 3.s :- seconds - 4.BWT :- Number of Bandwidth throttled requests - 5.IT :- Number of IOPS throttled requests - 6.OT :- Number of Other throttled requests - 7.NFR :- Number of requests which failed due to network errors - 8.%RT :- Percentage of requests that are throttled - 9.TRNR :- Total number of requests which succeeded without retrying - 10.TRF :- Total number of requests which failed - 11.TR :- Total number of requests which were made - 12.MRC :- Max retry count across all requests */ - @Override - public String toString() { - if (getMetricValue(TOTAL_NUMBER_OF_REQUESTS) == 0) { - return ""; + private void getRetryMetrics(StringBuilder metricBuilder) { + for (RetryValue retryCount : RETRY_LIST) { + long totalRequests = getMetricValue(TOTAL_REQUESTS, retryCount); + metricBuilder.append("$RCTSI$_").append(retryCount.getValue()) + .append("R=").append(getMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED, retryCount)); + + if (totalRequests > 0) { + metricBuilder.append("$MMA$_").append(retryCount.getValue()) + .append("R=").append(formatWithPrecision((double) getMetricValue(MIN_BACK_OFF, retryCount) / THOUSAND)).append("s") + .append(formatWithPrecision((double) getMetricValue(MAX_BACK_OFF, retryCount) / THOUSAND)).append("s") + .append(formatWithPrecision((double) getMetricValue(TOTAL_BACK_OFF, retryCount) / totalRequests / THOUSAND)).append("s"); + } else { + metricBuilder.append("$MMA$_").append(retryCount.getValue()).append("R=0s"); + } } - StringBuilder metricString = new StringBuilder(); + } + + /* + Acronyms :- + 1.BWT :- Number of Bandwidth throttled requests + 2.IT :- Number of IOPS throttled requests + 3.OT :- Number of Other throttled requests + 4.NFR :- Number of requests which failed due to network errors + 5.%RT :- Percentage of requests that are throttled + 6.TRNR :- Total number of requests which succeeded without retrying + 7.TRF :- Total number of requests which failed + 8.TR :- Total number of requests which were made + 9.MRC :- Max retry count across all requests + */ + private void getMmaMetrics(StringBuilder metricBuilder) { long totalRequestsThrottled = getMetricValue(NUMBER_OF_NETWORK_FAILED_REQUESTS) + getMetricValue(NUMBER_OF_IOPS_THROTTLED_REQUESTS) + getMetricValue(NUMBER_OF_OTHER_THROTTLED_REQUESTS) + getMetricValue(NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS); double percentageOfRequestsThrottled = ((double) totalRequestsThrottled / getMetricValue(TOTAL_NUMBER_OF_REQUESTS)) * HUNDRED; - for (RetryValue retryCount : RETRY_LIST) { - long totalRequests = getMetricValue(TOTAL_REQUESTS, retryCount); - metricString.append("$RCTSI$_").append(retryCount.getValue()).append("R=").append(getMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED, retryCount)); - if (totalRequests > 0) { - metricString.append("$MMA$_").append(retryCount.getValue()).append("R=") - .append(String.format("%.3f", (double) getMetricValue(MIN_BACK_OFF, retryCount) / THOUSAND)).append("s") - .append(String.format("%.3f", (double) getMetricValue(MAX_BACK_OFF, retryCount) / THOUSAND)).append("s") - .append(String.format("%.3f", (double) getMetricValue(TOTAL_BACK_OFF, retryCount) / totalRequests / THOUSAND)).append("s"); - } else { - metricString.append("$MMA$_").append(retryCount.getValue()).append("R=0s"); - } - } - metricString.append("$BWT=").append(getMetricValue(NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS)) + metricBuilder.append("$BWT=").append(getMetricValue(NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS)) .append("$IT=").append(getMetricValue(NUMBER_OF_IOPS_THROTTLED_REQUESTS)) .append("$OT=").append(getMetricValue(NUMBER_OF_OTHER_THROTTLED_REQUESTS)) - .append("$RT=").append(String.format("%.3f", percentageOfRequestsThrottled)) + .append("$RT=").append(formatWithPrecision(percentageOfRequestsThrottled)) .append("$NFR=").append(getMetricValue(NUMBER_OF_NETWORK_FAILED_REQUESTS)) .append("$TRNR=").append(getMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING)) .append("$TRF=").append(getMetricValue(NUMBER_OF_REQUESTS_FAILED)) .append("$TR=").append(getMetricValue(TOTAL_NUMBER_OF_REQUESTS)) .append("$MRC=").append(getMetricValue(MAX_RETRY_COUNT)); + } - return metricString.toString(); + /** + * Retrieves the string representation of the metrics. + * + * @return the string representation of the metrics + */ + @Override + public String toString() { + if (getMetricValue(TOTAL_NUMBER_OF_REQUESTS) == 0) { + return EMPTY_STRING; + } + StringBuilder metricBuilder = new StringBuilder(); + getRetryMetrics(metricBuilder); + getMmaMetrics(metricBuilder); + return metricBuilder.toString(); } @VisibleForTesting diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java index ddec755b482f7..8a20b7454cbeb 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java @@ -46,6 +46,7 @@ import static org.apache.hadoop.fs.azurebfs.enums.FileType.NON_PARQUET; import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_COUNTER; import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_GAUGE; +import static org.apache.hadoop.fs.azurebfs.utils.StringUtils.formatWithPrecision; import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.iostatisticsStore; /** @@ -58,7 +59,7 @@ public class AbfsReadFooterMetrics extends AbstractAbfsStatisticsSource { /** * Inner class to handle file type checks. */ - private static final class CheckFileType { + private static final class FileTypeMetrics { private final AtomicBoolean collectMetrics; private final AtomicBoolean collectMetricsForNextRead; private final AtomicBoolean collectLenMetrics; @@ -68,7 +69,7 @@ private static final class CheckFileType { private String sizeReadByFirstRead; private String offsetDiffBetweenFirstAndSecondRead; - private CheckFileType() { + private FileTypeMetrics() { collectMetrics = new AtomicBoolean(false); collectMetricsForNextRead = new AtomicBoolean(false); collectLenMetrics = new AtomicBoolean(false); @@ -151,7 +152,7 @@ private FileType getFileType() { } } - private final Map checkFileMap = new HashMap<>(); + private final Map fileTypeMetricsMap = new HashMap<>(); /** * Constructor to initialize the IOStatisticsStore with counters and gauges. @@ -230,7 +231,7 @@ public Long getTotalReadCount() { * @param filePathIdentifier the file path identifier */ public void updateMap(String filePathIdentifier) { - checkFileMap.computeIfAbsent(filePathIdentifier, key -> new CheckFileType()); + fileTypeMetricsMap.computeIfAbsent(filePathIdentifier, key -> new FileTypeMetrics()); } /** @@ -242,68 +243,104 @@ public void updateMap(String filePathIdentifier) { * @param nextReadPos the position of the next read */ public void checkMetricUpdate(final String filePathIdentifier, final int len, final long contentLength, final long nextReadPos) { - CheckFileType checkFileType = checkFileMap.computeIfAbsent(filePathIdentifier, key -> new CheckFileType()); - if (checkFileType.getReadCount() == 0 || (checkFileType.getReadCount() >= 1 && checkFileType.getCollectMetrics())) { - updateMetrics(checkFileType, len, contentLength, nextReadPos); + FileTypeMetrics fileTypeMetrics = fileTypeMetricsMap.computeIfAbsent(filePathIdentifier, key -> new FileTypeMetrics()); + if (fileTypeMetrics.getReadCount() == 0 || (fileTypeMetrics.getReadCount() >= 1 && fileTypeMetrics.getCollectMetrics())) { + updateMetrics(fileTypeMetrics, len, contentLength, nextReadPos); } } /** * Updates metrics for a specific file identified by filePathIdentifier. * - * @param checkFileType File metadata to know file type. + * @param fileTypeMetrics File metadata to know file type. * @param len The length of the read operation. * @param contentLength The total content length of the file. * @param nextReadPos The position of the next read operation. */ - private void updateMetrics(CheckFileType checkFileType, int len, long contentLength, long nextReadPos) { + private void updateMetrics(FileTypeMetrics fileTypeMetrics, int len, long contentLength, long nextReadPos) { synchronized (this) { - checkFileType.incrementReadCount(); + fileTypeMetrics.incrementReadCount(); } - long readCount = checkFileType.getReadCount(); + long readCount = fileTypeMetrics.getReadCount(); if (readCount == 1) { - handleFirstRead(checkFileType, nextReadPos, len, contentLength); + handleFirstRead(fileTypeMetrics, nextReadPos, len, contentLength); } else if (readCount == 2) { - handleSecondRead(checkFileType, nextReadPos, len, contentLength); + handleSecondRead(fileTypeMetrics, nextReadPos, len, contentLength); } else { - handleFurtherRead(checkFileType, len); + handleFurtherRead(fileTypeMetrics, len); } } - private void handleFirstRead(CheckFileType checkFileType, long nextReadPos, int len, long contentLength) { + /** + * Handles the first read operation by checking if the current read position is near the end of the file. + * If it is, updates the {@link FileTypeMetrics} object to enable metrics collection and records the first read's + * offset and size. + * + * @param fileTypeMetrics The {@link FileTypeMetrics} object to update with metrics and read details. + * @param nextReadPos The position where the next read will start. + * @param len The length of the current read operation. + * @param contentLength The total length of the file content. + */ + private void handleFirstRead(FileTypeMetrics fileTypeMetrics, long nextReadPos, int len, long contentLength) { if (nextReadPos >= contentLength - (long) Integer.parseInt(FOOTER_LENGTH) * ONE_KB) { - checkFileType.setCollectMetrics(true); - checkFileType.setCollectMetricsForNextRead(true); - checkFileType.setOffsetOfFirstRead(nextReadPos); - checkFileType.setSizeReadByFirstRead(len + "_" + Math.abs(contentLength - nextReadPos)); + fileTypeMetrics.setCollectMetrics(true); + fileTypeMetrics.setCollectMetricsForNextRead(true); + fileTypeMetrics.setOffsetOfFirstRead(nextReadPos); + fileTypeMetrics.setSizeReadByFirstRead(len + "_" + Math.abs(contentLength - nextReadPos)); } } - private void handleSecondRead(CheckFileType checkFileType, long nextReadPos, int len, long contentLength) { - if (checkFileType.getCollectMetricsForNextRead()) { - long offsetDiff = Math.abs(nextReadPos - checkFileType.getOffsetOfFirstRead()); - checkFileType.setOffsetDiffBetweenFirstAndSecondRead(len + "_" + offsetDiff); - checkFileType.setCollectLenMetrics(true); - checkFileType.updateFileType(); - updateMetricsData(checkFileType, len, contentLength); + /** + * Handles the second read operation by checking if metrics collection is enabled for the next read. + * If it is, calculates the offset difference between the first and second reads, updates the {@link FileTypeMetrics} + * object with this information, and sets the file type. Then, updates the metrics data. + * + * @param fileTypeMetrics The {@link FileTypeMetrics} object to update with metrics and read details. + * @param nextReadPos The position where the next read will start. + * @param len The length of the current read operation. + * @param contentLength The total length of the file content. + */ + private void handleSecondRead(FileTypeMetrics fileTypeMetrics, long nextReadPos, int len, long contentLength) { + if (fileTypeMetrics.getCollectMetricsForNextRead()) { + long offsetDiff = Math.abs(nextReadPos - fileTypeMetrics.getOffsetOfFirstRead()); + fileTypeMetrics.setOffsetDiffBetweenFirstAndSecondRead(len + "_" + offsetDiff); + fileTypeMetrics.setCollectLenMetrics(true); + fileTypeMetrics.updateFileType(); + updateMetricsData(fileTypeMetrics, len, contentLength); } } - private synchronized void handleFurtherRead(CheckFileType checkFileType, int len) { - if (checkFileType.getCollectLenMetrics() && checkFileType.getFileType() != null) { - FileType fileType = checkFileType.getFileType(); + /** + * Handles further read operations beyond the second read. If metrics collection is enabled and the file type is set, + * updates the read length requested and increments the read count for the specific file type. + * + * @param fileTypeMetrics The {@link FileTypeMetrics} object containing metrics and read details. + * @param len The length of the current read operation. + */ + private synchronized void handleFurtherRead(FileTypeMetrics fileTypeMetrics, int len) { + if (fileTypeMetrics.getCollectLenMetrics() && fileTypeMetrics.getFileType() != null) { + FileType fileType = fileTypeMetrics.getFileType(); updateMetricValue(fileType, READ_LEN_REQUESTED, len); incrementMetricValue(fileType, READ_COUNT); } } - private synchronized void updateMetricsData(CheckFileType checkFileType, int len, long contentLength) { - long sizeReadByFirstRead = Long.parseLong(checkFileType.getSizeReadByFirstRead().split("_")[0]); - long firstOffsetDiff = Long.parseLong(checkFileType.getSizeReadByFirstRead().split("_")[1]); - long secondOffsetDiff = Long.parseLong(checkFileType.getOffsetDiffBetweenFirstAndSecondRead().split("_")[1]); - FileType fileType = checkFileType.getFileType(); + /** + * Updates the metrics data for a specific file identified by the {@link FileTypeMetrics} object. + * This method calculates and updates various metrics such as read length requested, file length, + * size read by the first read, and offset differences between reads. + * + * @param fileTypeMetrics The {@link FileTypeMetrics} object containing metrics and read details. + * @param len The length of the current read operation. + * @param contentLength The total length of the file content. + */ + private synchronized void updateMetricsData(FileTypeMetrics fileTypeMetrics, int len, long contentLength) { + long sizeReadByFirstRead = Long.parseLong(fileTypeMetrics.getSizeReadByFirstRead().split("_")[0]); + long firstOffsetDiff = Long.parseLong(fileTypeMetrics.getSizeReadByFirstRead().split("_")[1]); + long secondOffsetDiff = Long.parseLong(fileTypeMetrics.getOffsetDiffBetweenFirstAndSecondRead().split("_")[1]); + FileType fileType = fileTypeMetrics.getFileType(); updateMetricValue(fileType, READ_LEN_REQUESTED, len + sizeReadByFirstRead); updateMetricValue(fileType, FILE_LENGTH, contentLength); @@ -314,18 +351,6 @@ private synchronized void updateMetricsData(CheckFileType checkFileType, int len incrementMetricValue(fileType, TOTAL_FILES); } - /** - * Returns the read footer metrics for a given file type. - * - * @param fileType the type of the file - * @return the read footer metrics as a string - */ - public String getReadFooterMetrics(FileType fileType) { - StringBuilder readFooterMetric = new StringBuilder(); - appendMetrics(readFooterMetric, fileType); - return readFooterMetric.toString(); - } - private void appendMetrics(StringBuilder metricBuilder, FileType fileType) { long totalFiles = getMetricValue(fileType, TOTAL_FILES); long readCount = getMetricValue(fileType, READ_COUNT); @@ -333,20 +358,19 @@ private void appendMetrics(StringBuilder metricBuilder, FileType fileType) { return; } - String sizeReadByFirstRead = String.format("%.3f", getMetricValue(fileType, SIZE_READ_BY_FIRST_READ) / (double) totalFiles); - String offsetDiffBetweenFirstAndSecondRead = String.format("%.3f", - getMetricValue(fileType, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ) / (double) totalFiles); + String sizeReadByFirstRead = formatWithPrecision(getMetricValue(fileType, SIZE_READ_BY_FIRST_READ) / (double) totalFiles); + String offsetDiffBetweenFirstAndSecondRead = formatWithPrecision(getMetricValue(fileType, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ) / (double) totalFiles); if (NON_PARQUET.equals(fileType)) { - sizeReadByFirstRead += "_" + String.format("%.3f", getMetricValue(fileType, FIRST_OFFSET_DIFF) / (double) totalFiles); - offsetDiffBetweenFirstAndSecondRead += "_" + String.format("%.3f", getMetricValue(fileType, SECOND_OFFSET_DIFF) / (double) totalFiles); + sizeReadByFirstRead += "_" + formatWithPrecision(getMetricValue(fileType, FIRST_OFFSET_DIFF) / (double) totalFiles); + offsetDiffBetweenFirstAndSecondRead += "_" + formatWithPrecision(getMetricValue(fileType, SECOND_OFFSET_DIFF) / (double) totalFiles); } metricBuilder.append("$").append(fileType) .append(":$FR=").append(sizeReadByFirstRead) .append("$SR=").append(offsetDiffBetweenFirstAndSecondRead) - .append("$FL=").append(String.format("%.3f", getMetricValue(fileType, FILE_LENGTH) / (double) totalFiles)) - .append("$RL=").append(String.format("%.3f", getMetricValue(fileType, READ_LEN_REQUESTED) / (double) readCount)); + .append("$FL=").append(formatWithPrecision(getMetricValue(fileType, FILE_LENGTH) / (double) totalFiles)) + .append("$RL=").append(formatWithPrecision(getMetricValue(fileType, READ_LEN_REQUESTED) / (double) readCount)); } /** diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java index a5bc7efd7e4bb..416a4bed491a5 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java @@ -18,6 +18,7 @@ package org.apache.hadoop.fs.azurebfs.statistics; +import org.apache.hadoop.fs.statistics.IOStatistics; import org.apache.hadoop.fs.statistics.IOStatisticsSource; import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; @@ -25,7 +26,7 @@ * Abstract class for Abfs statistics source. */ public abstract class AbstractAbfsStatisticsSource implements IOStatisticsSource { - private IOStatisticsStore ioStatistics; + private IOStatisticsStore ioStatisticsStore; /** * Default constructor. @@ -39,17 +40,17 @@ protected AbstractAbfsStatisticsSource() { * @return the IOStatisticsStore instance */ @Override - public IOStatisticsStore getIOStatistics() { - return ioStatistics; + public IOStatistics getIOStatistics() { + return ioStatisticsStore; } /** * Sets the IOStatisticsStore instance. * - * @param ioStatistics the IOStatisticsStore instance to set + * @param ioStatisticsStore the IOStatisticsStore instance to set */ - protected void setIOStatistics(final IOStatisticsStore ioStatistics) { - this.ioStatistics = ioStatistics; + protected void setIOStatistics(final IOStatisticsStore ioStatisticsStore) { + this.ioStatisticsStore = ioStatisticsStore; } /** @@ -68,7 +69,7 @@ public void incCounterValue(String name) { * @param value the value to increment by */ public void incCounterValue(String name, long value) { - ioStatistics.incrementCounter(name, value); + ioStatisticsStore.incrementCounter(name, value); } /** @@ -78,7 +79,7 @@ public void incCounterValue(String name, long value) { * @return the counter value */ public Long lookupCounterValue(String name) { - return ioStatistics.counters().getOrDefault(name, 0L); + return ioStatisticsStore.counters().getOrDefault(name, 0L); } /** @@ -88,17 +89,7 @@ public Long lookupCounterValue(String name) { * @param value the value to set */ public void setCounterValue(String name, long value) { - ioStatistics.setCounter(name, value); - } - - /** - * Updates the counter value by adding the specified value to the current value for the given name. - * - * @param name the name of the counter - * @param value the value to add - */ - public void updateCounterValue(String name, long value) { - ioStatistics.setCounter(name, lookupCounterValue(name) + value); + ioStatisticsStore.setCounter(name, value); } /** @@ -110,16 +101,6 @@ public void incGaugeValue(String name) { incCounterValue(name, 1); } - /** - * Increments the gauge value by the specified value for the given name. - * - * @param name the name of the gauge - * @param value the value to increment by - */ - public void incGaugeValue(String name, long value) { - ioStatistics.incrementGauge(name, value); - } - /** * Looks up the gauge value for the given name. * @@ -127,7 +108,7 @@ public void incGaugeValue(String name, long value) { * @return the gauge value */ public Long lookupGaugeValue(String name) { - return ioStatistics.gauges().getOrDefault(name, 0L); + return ioStatisticsStore.gauges().getOrDefault(name, 0L); } /** @@ -137,7 +118,7 @@ public Long lookupGaugeValue(String name) { * @param value the value to add */ public void updateGaugeValue(String name, long value) { - ioStatistics.setGauge(name, lookupGaugeValue(name) + value); + ioStatisticsStore.setGauge(name, lookupGaugeValue(name) + value); } /** @@ -147,7 +128,7 @@ public void updateGaugeValue(String name, long value) { * @param value the value to set */ public void setGaugeValue(String name, long value) { - ioStatistics.setGauge(name, value); + ioStatisticsStore.setGauge(name, value); } /** @@ -157,6 +138,6 @@ public void setGaugeValue(String name, long value) { */ @Override public String toString() { - return "AbstractAbfsStatisticsStore{" + ioStatistics + '}'; + return "AbstractAbfsStatisticsStore{" + ioStatisticsStore + '}'; } } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/StringUtils.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/StringUtils.java new file mode 100644 index 0000000000000..4680acbd92728 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/StringUtils.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.hadoop.fs.azurebfs.utils; + +public class StringUtils { + + /** + * Formats a double value to a string with three decimal places. + * + * @param value The double value to format. + * @return A string representation of the double value with three decimal places. + */ + public static String formatWithPrecision(double value) { + return String.format("%.3f", value); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsBackoffMetrics.java index b605fb8134997..cb20bc1861dc2 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsBackoffMetrics.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsBackoffMetrics.java @@ -35,11 +35,17 @@ public class TestAbfsBackoffMetrics { private static final int TOTAL_COUNTERS = 22; private static final int TOTAL_GAUGES = 21; + /** + * Sets up the test environment by initializing the AbfsBackoffMetrics instance. + */ @Before public void setUp() { metrics = new AbfsBackoffMetrics(); } + /** + * Tests the retrieval of metric names based on the statistic type. + */ @Test public void retrievesMetricNamesBasedOnStatisticType() { String[] counterMetrics = metrics.getMetricNamesByType(TYPE_COUNTER); @@ -52,6 +58,9 @@ public void retrievesMetricNamesBasedOnStatisticType() { .isEqualTo(TOTAL_GAUGES); } + /** + * Tests the retrieval of the value of a specific metric. + */ @Test public void retrievesValueOfSpecificMetric() { metrics.setMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED, 5, ONE); @@ -63,6 +72,9 @@ public void retrievesValueOfSpecificMetric() { .isEqualTo(0); } + /** + * Tests the increment of the value of a specific metric. + */ @Test public void incrementsValueOfSpecificMetric() { metrics.incrementMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED, ONE); @@ -74,6 +86,9 @@ public void incrementsValueOfSpecificMetric() { .isEqualTo(0); } + /** + * Tests the string representation of empty backoff metrics. + */ @Test public void returnsStringRepresentationOfEmptyBackoffMetrics() { Assertions.assertThat(metrics.getMetricValue(TOTAL_NUMBER_OF_REQUESTS)) @@ -84,14 +99,17 @@ public void returnsStringRepresentationOfEmptyBackoffMetrics() { .isEmpty(); } + /** + * Tests the string representation of backoff metrics. + */ @Test public void returnsStringRepresentationOfBackoffMetrics() { metrics.incrementMetricValue(TOTAL_NUMBER_OF_REQUESTS); Assertions.assertThat(metrics.getMetricValue(TOTAL_NUMBER_OF_REQUESTS)) - .describedAs("String representation of backoff metrics should be empty") + .describedAs("String representation of backoff metrics should not be empty") .isEqualTo(1); Assertions.assertThat(metrics.toString()) - .describedAs("String representation of backoff metrics should be empty") + .describedAs("String representation of backoff metrics should not be empty") .contains("$TR=1"); } } From e4b680acad6278afd926ca4c075ff80192d27f59 Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Mon, 18 Nov 2024 03:15:39 -0800 Subject: [PATCH 20/32] Checkstype fixes --- .../hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java | 3 ++- .../java/org/apache/hadoop/fs/azurebfs/utils/StringUtils.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java index 8a20b7454cbeb..f6fad71479f7c 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java @@ -359,7 +359,8 @@ private void appendMetrics(StringBuilder metricBuilder, FileType fileType) { } String sizeReadByFirstRead = formatWithPrecision(getMetricValue(fileType, SIZE_READ_BY_FIRST_READ) / (double) totalFiles); - String offsetDiffBetweenFirstAndSecondRead = formatWithPrecision(getMetricValue(fileType, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ) / (double) totalFiles); + String offsetDiffBetweenFirstAndSecondRead = formatWithPrecision(getMetricValue(fileType, + OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ) / (double) totalFiles); if (NON_PARQUET.equals(fileType)) { sizeReadByFirstRead += "_" + formatWithPrecision(getMetricValue(fileType, FIRST_OFFSET_DIFF) / (double) totalFiles); diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/StringUtils.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/StringUtils.java index 4680acbd92728..424d889ddfaf8 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/StringUtils.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/StringUtils.java @@ -19,7 +19,7 @@ package org.apache.hadoop.fs.azurebfs.utils; -public class StringUtils { +public final class StringUtils { /** * Formats a double value to a string with three decimal places. From 3cc57f7a55728b189bf6496d1beb55b1a0a07506 Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Mon, 18 Nov 2024 07:42:30 -0800 Subject: [PATCH 21/32] Add private constructor in StringUtils class --- .../org/apache/hadoop/fs/azurebfs/utils/StringUtils.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/StringUtils.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/StringUtils.java index 424d889ddfaf8..0bd17bbf41d98 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/StringUtils.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/StringUtils.java @@ -30,4 +30,10 @@ public final class StringUtils { public static String formatWithPrecision(double value) { return String.format("%.3f", value); } + + /** + * Private constructor to prevent instantiation. + */ + private StringUtils() { + } } From f636f41fb36b891fd83c5efa243b9bbbd30a22ab Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Mon, 9 Dec 2024 01:52:15 -0800 Subject: [PATCH 22/32] Changes based on comments given --- .../azurebfs/constants/MetricsConstants.java | 1 + .../azurebfs/services/AbfsBackoffMetrics.java | 15 +++---- .../services/AbfsReadFooterMetrics.java | 15 +++---- .../hadoop/fs/azurebfs/utils/StringUtils.java | 39 ------------------- .../services/TestAbfsBackoffMetrics.java | 2 +- 5 files changed, 18 insertions(+), 54 deletions(-) delete mode 100644 hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/StringUtils.java diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java index 6b8a742314584..913d5345397f7 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java @@ -29,6 +29,7 @@ public final class MetricsConstants { public static final String RETRY = "RETRY"; public static final String BASE = "BASE"; public static final String FILE = "FILE"; + public static final String DOUBLE_PRECISION_FORMAT = "%.3f"; // Private constructor to prevent instantiation private MetricsConstants() { diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java index 32187ef78e68c..29514a4dcf059 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java @@ -34,6 +34,7 @@ import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.THOUSAND; import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.RETRY; import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.COLON; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.DOUBLE_PRECISION_FORMAT; import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.MAX_BACK_OFF; import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.MIN_BACK_OFF; import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.MAX_RETRY_COUNT; @@ -56,8 +57,8 @@ import static org.apache.hadoop.fs.azurebfs.enums.RetryValue.TWENTY_FIVE_AND_ABOVE; import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_COUNTER; import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_GAUGE; -import static org.apache.hadoop.fs.azurebfs.utils.StringUtils.formatWithPrecision; import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.iostatisticsStore; +import static org.apache.hadoop.util.StringUtils.format; /** * This class is responsible for tracking and @@ -213,9 +214,9 @@ private void getRetryMetrics(StringBuilder metricBuilder) { if (totalRequests > 0) { metricBuilder.append("$MMA$_").append(retryCount.getValue()) - .append("R=").append(formatWithPrecision((double) getMetricValue(MIN_BACK_OFF, retryCount) / THOUSAND)).append("s") - .append(formatWithPrecision((double) getMetricValue(MAX_BACK_OFF, retryCount) / THOUSAND)).append("s") - .append(formatWithPrecision((double) getMetricValue(TOTAL_BACK_OFF, retryCount) / totalRequests / THOUSAND)).append("s"); + .append("R=").append(format(DOUBLE_PRECISION_FORMAT, (double) getMetricValue(MIN_BACK_OFF, retryCount) / THOUSAND)).append("s") + .append(format(DOUBLE_PRECISION_FORMAT, (double) getMetricValue(MAX_BACK_OFF, retryCount) / THOUSAND)).append("s") + .append(format(DOUBLE_PRECISION_FORMAT, ((double) getMetricValue(TOTAL_BACK_OFF, retryCount) / totalRequests) / THOUSAND)).append("s"); } else { metricBuilder.append("$MMA$_").append(retryCount.getValue()).append("R=0s"); } @@ -234,7 +235,7 @@ private void getRetryMetrics(StringBuilder metricBuilder) { 8.TR :- Total number of requests which were made 9.MRC :- Max retry count across all requests */ - private void getMmaMetrics(StringBuilder metricBuilder) { + private void getBaseMetrics(StringBuilder metricBuilder) { long totalRequestsThrottled = getMetricValue(NUMBER_OF_NETWORK_FAILED_REQUESTS) + getMetricValue(NUMBER_OF_IOPS_THROTTLED_REQUESTS) + getMetricValue(NUMBER_OF_OTHER_THROTTLED_REQUESTS) @@ -244,7 +245,7 @@ private void getMmaMetrics(StringBuilder metricBuilder) { metricBuilder.append("$BWT=").append(getMetricValue(NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS)) .append("$IT=").append(getMetricValue(NUMBER_OF_IOPS_THROTTLED_REQUESTS)) .append("$OT=").append(getMetricValue(NUMBER_OF_OTHER_THROTTLED_REQUESTS)) - .append("$RT=").append(formatWithPrecision(percentageOfRequestsThrottled)) + .append("$RT=").append(format(DOUBLE_PRECISION_FORMAT, percentageOfRequestsThrottled)) .append("$NFR=").append(getMetricValue(NUMBER_OF_NETWORK_FAILED_REQUESTS)) .append("$TRNR=").append(getMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING)) .append("$TRF=").append(getMetricValue(NUMBER_OF_REQUESTS_FAILED)) @@ -264,7 +265,7 @@ public String toString() { } StringBuilder metricBuilder = new StringBuilder(); getRetryMetrics(metricBuilder); - getMmaMetrics(metricBuilder); + getBaseMetrics(metricBuilder); return metricBuilder.toString(); } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java index f6fad71479f7c..2f72aa7dac25e 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java @@ -34,6 +34,7 @@ import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.ONE_KB; import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.FILE; import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.COLON; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.DOUBLE_PRECISION_FORMAT; import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.TOTAL_FILES; import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.FILE_LENGTH; import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.SIZE_READ_BY_FIRST_READ; @@ -46,8 +47,8 @@ import static org.apache.hadoop.fs.azurebfs.enums.FileType.NON_PARQUET; import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_COUNTER; import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_GAUGE; -import static org.apache.hadoop.fs.azurebfs.utils.StringUtils.formatWithPrecision; import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.iostatisticsStore; +import static org.apache.hadoop.util.StringUtils.format; /** * This class is responsible for tracking and updating metrics related to reading footers in files. @@ -358,20 +359,20 @@ private void appendMetrics(StringBuilder metricBuilder, FileType fileType) { return; } - String sizeReadByFirstRead = formatWithPrecision(getMetricValue(fileType, SIZE_READ_BY_FIRST_READ) / (double) totalFiles); - String offsetDiffBetweenFirstAndSecondRead = formatWithPrecision(getMetricValue(fileType, + String sizeReadByFirstRead = format(DOUBLE_PRECISION_FORMAT, getMetricValue(fileType, SIZE_READ_BY_FIRST_READ) / (double) totalFiles); + String offsetDiffBetweenFirstAndSecondRead = format(DOUBLE_PRECISION_FORMAT, getMetricValue(fileType, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ) / (double) totalFiles); if (NON_PARQUET.equals(fileType)) { - sizeReadByFirstRead += "_" + formatWithPrecision(getMetricValue(fileType, FIRST_OFFSET_DIFF) / (double) totalFiles); - offsetDiffBetweenFirstAndSecondRead += "_" + formatWithPrecision(getMetricValue(fileType, SECOND_OFFSET_DIFF) / (double) totalFiles); + sizeReadByFirstRead += "_" + format(DOUBLE_PRECISION_FORMAT, getMetricValue(fileType, FIRST_OFFSET_DIFF) / (double) totalFiles); + offsetDiffBetweenFirstAndSecondRead += "_" + format(DOUBLE_PRECISION_FORMAT, getMetricValue(fileType, SECOND_OFFSET_DIFF) / (double) totalFiles); } metricBuilder.append("$").append(fileType) .append(":$FR=").append(sizeReadByFirstRead) .append("$SR=").append(offsetDiffBetweenFirstAndSecondRead) - .append("$FL=").append(formatWithPrecision(getMetricValue(fileType, FILE_LENGTH) / (double) totalFiles)) - .append("$RL=").append(formatWithPrecision(getMetricValue(fileType, READ_LEN_REQUESTED) / (double) readCount)); + .append("$FL=").append(format(DOUBLE_PRECISION_FORMAT, getMetricValue(fileType, FILE_LENGTH) / (double) totalFiles)) + .append("$RL=").append(format(DOUBLE_PRECISION_FORMAT, getMetricValue(fileType, READ_LEN_REQUESTED) / (double) readCount)); } /** diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/StringUtils.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/StringUtils.java deleted file mode 100644 index 0bd17bbf41d98..0000000000000 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/StringUtils.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package org.apache.hadoop.fs.azurebfs.utils; - -public final class StringUtils { - - /** - * Formats a double value to a string with three decimal places. - * - * @param value The double value to format. - * @return A string representation of the double value with three decimal places. - */ - public static String formatWithPrecision(double value) { - return String.format("%.3f", value); - } - - /** - * Private constructor to prevent instantiation. - */ - private StringUtils() { - } -} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsBackoffMetrics.java index cb20bc1861dc2..02b25520877f3 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsBackoffMetrics.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsBackoffMetrics.java @@ -54,7 +54,7 @@ public void retrievesMetricNamesBasedOnStatisticType() { .describedAs("Counter metrics should have 22 elements") .isEqualTo(TOTAL_COUNTERS); Assertions.assertThat(gaugeMetrics.length) - .describedAs("Counter metrics should have 21 elements") + .describedAs("Gauge metrics should have 21 elements") .isEqualTo(TOTAL_GAUGES); } From 154a4ae36b4df01195e11c04219a81621982cfc4 Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Mon, 9 Dec 2024 03:52:51 -0800 Subject: [PATCH 23/32] Use of Mean statistics in read footer metrics --- .../hadoop/fs/azurebfs/AbfsCountersImpl.java | 2 +- .../enums/AbfsReadFooterMetricsEnum.java | 15 ++- .../fs/azurebfs/enums/StatisticTypeEnum.java | 6 +- .../azurebfs/services/AbfsBackoffMetrics.java | 4 +- .../services/AbfsReadFooterMetrics.java | 93 ++++++++----------- .../AbstractAbfsStatisticsSource.java | 32 +++---- .../azurebfs/ITestAbfsReadFooterMetrics.java | 34 ++++--- .../services/TestAbfsReadFooterMetrics.java | 12 --- 8 files changed, 85 insertions(+), 113 deletions(-) diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java index 9f359a6cf1acb..fdcbc2275ff48 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsCountersImpl.java @@ -332,7 +332,7 @@ public String toString() { } } if (abfsReadFooterMetrics != null) { - if (getAbfsReadFooterMetrics().getTotalFiles() > 0 && getAbfsReadFooterMetrics().getTotalReadCount() > 0) { + if (getAbfsReadFooterMetrics().getTotalFiles() > 0) { metric += "#FO:" + getAbfsReadFooterMetrics().toString(); } } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java index afd5da27c395f..e14b66189ccec 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java @@ -20,20 +20,19 @@ import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.FILE; import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_COUNTER; -import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_GAUGE; +import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_MEAN; /** * Enum representing various ABFS read footer metrics. */ public enum AbfsReadFooterMetricsEnum { TOTAL_FILES("totalFiles", "Total files read", FILE, TYPE_COUNTER), - FILE_LENGTH("fileLength", "File length", FILE, TYPE_GAUGE), - SIZE_READ_BY_FIRST_READ("sizeReadByFirstRead", "Size read by first read", FILE, TYPE_GAUGE), - OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ("offsetDiffBetweenFirstAndSecondRead", "Offset difference between first and second read", FILE, TYPE_GAUGE), - READ_LEN_REQUESTED("readLenRequested", "Read length requested", FILE, TYPE_GAUGE), - READ_COUNT("readCount", "Number of total reads", FILE, TYPE_COUNTER), - FIRST_OFFSET_DIFF("firstOffsetDiff", "First offset difference", FILE, TYPE_GAUGE), - SECOND_OFFSET_DIFF("secondOffsetDiff", "Second offset difference", FILE, TYPE_GAUGE); + AVG_FILE_LENGTH("avgFileLength", "Average File length", FILE, TYPE_MEAN), + AVG_SIZE_READ_BY_FIRST_READ("avgSizeReadByFirstRead", "Average Size read by first read", FILE, TYPE_MEAN), + AVG_OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ("avgOffsetDiffBetweenFirstAndSecondRead", "Average Offset difference between first and second read", FILE, TYPE_MEAN), + AVG_READ_LEN_REQUESTED("avgReadLenRequested", "Average Read length requested", FILE, TYPE_MEAN), + AVG_FIRST_OFFSET_DIFF("avgFirstOffsetDiff", "Average First offset difference", FILE, TYPE_MEAN), + AVG_SECOND_OFFSET_DIFF("avgSecondOffsetDiff", "Average Second offset difference", FILE, TYPE_MEAN); private final String name; private final String description; diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/StatisticTypeEnum.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/StatisticTypeEnum.java index 8fbe8048443f6..0d8471f874a69 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/StatisticTypeEnum.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/StatisticTypeEnum.java @@ -29,5 +29,9 @@ public enum StatisticTypeEnum { /** * Gauge. */ - TYPE_GAUGE + TYPE_GAUGE, + /** + * Mean. + */ + TYPE_MEAN } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java index 29514a4dcf059..d60e54164c940 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java @@ -59,6 +59,7 @@ import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_GAUGE; import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.iostatisticsStore; import static org.apache.hadoop.util.StringUtils.format; +import static org.apache.hadoop.util.StringUtils.formatPercent; /** * This class is responsible for tracking and @@ -240,12 +241,11 @@ private void getBaseMetrics(StringBuilder metricBuilder) { + getMetricValue(NUMBER_OF_IOPS_THROTTLED_REQUESTS) + getMetricValue(NUMBER_OF_OTHER_THROTTLED_REQUESTS) + getMetricValue(NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS); - double percentageOfRequestsThrottled = ((double) totalRequestsThrottled / getMetricValue(TOTAL_NUMBER_OF_REQUESTS)) * HUNDRED; metricBuilder.append("$BWT=").append(getMetricValue(NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS)) .append("$IT=").append(getMetricValue(NUMBER_OF_IOPS_THROTTLED_REQUESTS)) .append("$OT=").append(getMetricValue(NUMBER_OF_OTHER_THROTTLED_REQUESTS)) - .append("$RT=").append(format(DOUBLE_PRECISION_FORMAT, percentageOfRequestsThrottled)) + .append("$RT=").append(formatPercent(totalRequestsThrottled/ (double)getMetricValue(TOTAL_NUMBER_OF_REQUESTS), 3)) .append("$NFR=").append(getMetricValue(NUMBER_OF_NETWORK_FAILED_REQUESTS)) .append("$TRNR=").append(getMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING)) .append("$TRF=").append(getMetricValue(NUMBER_OF_REQUESTS_FAILED)) diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java index 2f72aa7dac25e..e83657a7f74a6 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java @@ -36,17 +36,16 @@ import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.COLON; import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.DOUBLE_PRECISION_FORMAT; import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.TOTAL_FILES; -import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.FILE_LENGTH; -import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.SIZE_READ_BY_FIRST_READ; -import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ; -import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.READ_LEN_REQUESTED; -import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.READ_COUNT; -import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.FIRST_OFFSET_DIFF; -import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.SECOND_OFFSET_DIFF; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.AVG_FILE_LENGTH; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.AVG_SIZE_READ_BY_FIRST_READ; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.AVG_OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.AVG_READ_LEN_REQUESTED; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.AVG_FIRST_OFFSET_DIFF; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.AVG_SECOND_OFFSET_DIFF; import static org.apache.hadoop.fs.azurebfs.enums.FileType.PARQUET; import static org.apache.hadoop.fs.azurebfs.enums.FileType.NON_PARQUET; import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_COUNTER; -import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_GAUGE; +import static org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum.TYPE_MEAN; import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.iostatisticsStore; import static org.apache.hadoop.util.StringUtils.format; @@ -156,12 +155,12 @@ private FileType getFileType() { private final Map fileTypeMetricsMap = new HashMap<>(); /** - * Constructor to initialize the IOStatisticsStore with counters and gauges. + * Constructor to initialize the IOStatisticsStore with counters and mean statistics. */ public AbfsReadFooterMetrics() { IOStatisticsStore ioStatisticsStore = iostatisticsStore() .withCounters(getMetricNames(TYPE_COUNTER)) - .withGauges(getMetricNames(TYPE_GAUGE)) + .withMeanStatistics(getMetricNames(TYPE_MEAN)) .build(); setIOStatistics(ioStatisticsStore); } @@ -176,26 +175,12 @@ private String[] getMetricNames(StatisticTypeEnum type) { .toArray(String[]::new); } - private long getMetricValue(FileType fileType, AbfsReadFooterMetricsEnum metric) { - switch (metric.getStatisticType()) { - case TYPE_COUNTER: - return lookupCounterValue(fileType + COLON + metric.getName()); - case TYPE_GAUGE: - return lookupGaugeValue(fileType + COLON + metric.getName()); - default: - return 0; - } + private long getCounterMetricValue(FileType fileType, AbfsReadFooterMetricsEnum metric) { + return lookupCounterValue(fileType + COLON + metric.getName()); } - /** - * Updates the value of a specific metric. - * - * @param fileType the type of the file - * @param metric the metric to update - * @param value the new value of the metric - */ - public void updateMetricValue(FileType fileType, AbfsReadFooterMetricsEnum metric, long value) { - updateGaugeValue(fileType + COLON + metric.getName(), value); + private String getMeanMetricValue(FileType fileType, AbfsReadFooterMetricsEnum metric) { + return format(DOUBLE_PRECISION_FORMAT, lookupMeanStatistic(fileType + COLON + metric.getName())); } /** @@ -209,21 +194,23 @@ public void incrementMetricValue(FileType fileType, AbfsReadFooterMetricsEnum me } /** - * Returns the total number of files. + * Adds a mean statistic value for a specific metric. * - * @return the total number of files + * @param fileType the type of the file + * @param metricName the metric to update + * @param value the new value of the metric */ - public Long getTotalFiles() { - return getMetricValue(PARQUET, TOTAL_FILES) + getMetricValue(NON_PARQUET, TOTAL_FILES); + public void addMeanMetricValue(FileType fileType, AbfsReadFooterMetricsEnum metricName, long value) { + addMeanStatistic(fileType + COLON + metricName, value); } /** - * Returns the total read count. + * Returns the total number of files. * - * @return the total read count + * @return the total number of files */ - public Long getTotalReadCount() { - return getMetricValue(PARQUET, READ_COUNT) + getMetricValue(NON_PARQUET, READ_COUNT); + public Long getTotalFiles() { + return getCounterMetricValue(PARQUET, TOTAL_FILES) + getCounterMetricValue(NON_PARQUET, TOTAL_FILES); } /** @@ -323,8 +310,7 @@ private void handleSecondRead(FileTypeMetrics fileTypeMetrics, long nextReadPos, private synchronized void handleFurtherRead(FileTypeMetrics fileTypeMetrics, int len) { if (fileTypeMetrics.getCollectLenMetrics() && fileTypeMetrics.getFileType() != null) { FileType fileType = fileTypeMetrics.getFileType(); - updateMetricValue(fileType, READ_LEN_REQUESTED, len); - incrementMetricValue(fileType, READ_COUNT); + addMeanMetricValue(fileType, AVG_READ_LEN_REQUESTED, len); } } @@ -343,36 +329,35 @@ private synchronized void updateMetricsData(FileTypeMetrics fileTypeMetrics, int long secondOffsetDiff = Long.parseLong(fileTypeMetrics.getOffsetDiffBetweenFirstAndSecondRead().split("_")[1]); FileType fileType = fileTypeMetrics.getFileType(); - updateMetricValue(fileType, READ_LEN_REQUESTED, len + sizeReadByFirstRead); - updateMetricValue(fileType, FILE_LENGTH, contentLength); - updateMetricValue(fileType, SIZE_READ_BY_FIRST_READ, sizeReadByFirstRead); - updateMetricValue(fileType, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ, len); - updateMetricValue(fileType, FIRST_OFFSET_DIFF, firstOffsetDiff); - updateMetricValue(fileType, SECOND_OFFSET_DIFF, secondOffsetDiff); + addMeanMetricValue(fileType, AVG_READ_LEN_REQUESTED, len); + addMeanMetricValue(fileType, AVG_READ_LEN_REQUESTED, sizeReadByFirstRead); + addMeanMetricValue(fileType, AVG_FILE_LENGTH, contentLength); + addMeanMetricValue(fileType, AVG_SIZE_READ_BY_FIRST_READ, sizeReadByFirstRead); + addMeanMetricValue(fileType, AVG_OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ, len); + addMeanMetricValue(fileType, AVG_FIRST_OFFSET_DIFF, firstOffsetDiff); + addMeanMetricValue(fileType, AVG_SECOND_OFFSET_DIFF, secondOffsetDiff); incrementMetricValue(fileType, TOTAL_FILES); } private void appendMetrics(StringBuilder metricBuilder, FileType fileType) { - long totalFiles = getMetricValue(fileType, TOTAL_FILES); - long readCount = getMetricValue(fileType, READ_COUNT); - if (totalFiles <= 0 || readCount <= 0) { + long totalFiles = getCounterMetricValue(fileType, TOTAL_FILES); + if (totalFiles <= 0) { return; } - String sizeReadByFirstRead = format(DOUBLE_PRECISION_FORMAT, getMetricValue(fileType, SIZE_READ_BY_FIRST_READ) / (double) totalFiles); - String offsetDiffBetweenFirstAndSecondRead = format(DOUBLE_PRECISION_FORMAT, getMetricValue(fileType, - OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ) / (double) totalFiles); + String sizeReadByFirstRead = getMeanMetricValue(fileType, AVG_SIZE_READ_BY_FIRST_READ); + String offsetDiffBetweenFirstAndSecondRead = getMeanMetricValue(fileType, AVG_OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ); if (NON_PARQUET.equals(fileType)) { - sizeReadByFirstRead += "_" + format(DOUBLE_PRECISION_FORMAT, getMetricValue(fileType, FIRST_OFFSET_DIFF) / (double) totalFiles); - offsetDiffBetweenFirstAndSecondRead += "_" + format(DOUBLE_PRECISION_FORMAT, getMetricValue(fileType, SECOND_OFFSET_DIFF) / (double) totalFiles); + sizeReadByFirstRead += "_" + getMeanMetricValue(fileType, AVG_FIRST_OFFSET_DIFF); + offsetDiffBetweenFirstAndSecondRead += "_" + getMeanMetricValue(fileType, AVG_SECOND_OFFSET_DIFF); } metricBuilder.append("$").append(fileType) .append(":$FR=").append(sizeReadByFirstRead) .append("$SR=").append(offsetDiffBetweenFirstAndSecondRead) - .append("$FL=").append(format(DOUBLE_PRECISION_FORMAT, getMetricValue(fileType, FILE_LENGTH) / (double) totalFiles)) - .append("$RL=").append(format(DOUBLE_PRECISION_FORMAT, getMetricValue(fileType, READ_LEN_REQUESTED) / (double) readCount)); + .append("$FL=").append(getMeanMetricValue(fileType, AVG_FILE_LENGTH)) + .append("$RL=").append(getMeanMetricValue(fileType, AVG_READ_LEN_REQUESTED)); } /** diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java index 416a4bed491a5..dfe49e8d8d813 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java @@ -58,7 +58,7 @@ protected void setIOStatistics(final IOStatisticsStore ioStatisticsStore) { * * @param name the name of the counter */ - public void incCounterValue(String name) { + protected void incCounterValue(String name) { incCounterValue(name, 1); } @@ -68,7 +68,7 @@ public void incCounterValue(String name) { * @param name the name of the counter * @param value the value to increment by */ - public void incCounterValue(String name, long value) { + protected void incCounterValue(String name, long value) { ioStatisticsStore.incrementCounter(name, value); } @@ -78,7 +78,7 @@ public void incCounterValue(String name, long value) { * @param name the name of the counter * @return the counter value */ - public Long lookupCounterValue(String name) { + protected Long lookupCounterValue(String name) { return ioStatisticsStore.counters().getOrDefault(name, 0L); } @@ -88,7 +88,7 @@ public Long lookupCounterValue(String name) { * @param name the name of the counter * @param value the value to set */ - public void setCounterValue(String name, long value) { + protected void setCounterValue(String name, long value) { ioStatisticsStore.setCounter(name, value); } @@ -97,7 +97,7 @@ public void setCounterValue(String name, long value) { * * @param name the name of the gauge */ - public void incGaugeValue(String name) { + protected void incGaugeValue(String name) { incCounterValue(name, 1); } @@ -107,30 +107,28 @@ public void incGaugeValue(String name) { * @param name the name of the gauge * @return the gauge value */ - public Long lookupGaugeValue(String name) { + protected Long lookupGaugeValue(String name) { return ioStatisticsStore.gauges().getOrDefault(name, 0L); } - /** - * Updates the gauge value by adding the specified value to the current value for the given name. - * - * @param name the name of the gauge - * @param value the value to add - */ - public void updateGaugeValue(String name, long value) { - ioStatisticsStore.setGauge(name, lookupGaugeValue(name) + value); - } - /** * Sets the gauge value for the given name. * * @param name the name of the gauge * @param value the value to set */ - public void setGaugeValue(String name, long value) { + protected void setGaugeValue(String name, long value) { ioStatisticsStore.setGauge(name, value); } + protected void addMeanStatistic(String name, long value) { + ioStatisticsStore.addMeanStatisticSample(name, value); + } + + protected double lookupMeanStatistic(String name) { + return ioStatisticsStore.meanStatistics().get(name).mean(); + } + /** * Returns a string representation of the AbstractAbfsStatisticsSource. * diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java index 8e73c637c889f..dac5e02a77a40 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java @@ -31,14 +31,13 @@ import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.DEFAULT_READ_BUFFER_SIZE; import static org.apache.hadoop.fs.azurebfs.enums.FileType.NON_PARQUET; import static org.apache.hadoop.fs.azurebfs.enums.FileType.PARQUET; -import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.FILE_LENGTH; -import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ; -import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.READ_COUNT; -import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.READ_LEN_REQUESTED; -import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.SIZE_READ_BY_FIRST_READ; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.AVG_FILE_LENGTH; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.AVG_OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.AVG_READ_LEN_REQUESTED; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.AVG_SIZE_READ_BY_FIRST_READ; import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.TOTAL_FILES; -import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.FIRST_OFFSET_DIFF; -import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.SECOND_OFFSET_DIFF; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.AVG_FIRST_OFFSET_DIFF; +import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.AVG_SECOND_OFFSET_DIFF; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; @@ -206,13 +205,12 @@ public void testReadFooterMetrics() throws Exception { */ private AbfsReadFooterMetrics getNonParquetMetrics() { AbfsReadFooterMetrics nonParquetMetrics = new AbfsReadFooterMetrics(); - nonParquetMetrics.incrementMetricValue(NON_PARQUET, READ_COUNT); - nonParquetMetrics.updateMetricValue(NON_PARQUET, FILE_LENGTH, Long.parseLong("32768")); - nonParquetMetrics.updateMetricValue(NON_PARQUET, READ_LEN_REQUESTED, Long.parseLong("32769")); - nonParquetMetrics.updateMetricValue(NON_PARQUET, SIZE_READ_BY_FIRST_READ, Long.parseLong("16384")); - nonParquetMetrics.updateMetricValue(NON_PARQUET, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ, 1); - nonParquetMetrics.updateMetricValue(NON_PARQUET, FIRST_OFFSET_DIFF, Long.parseLong("16384")); - nonParquetMetrics.updateMetricValue(NON_PARQUET, SECOND_OFFSET_DIFF, Long.parseLong("16384")); + nonParquetMetrics.addMeanMetricValue(NON_PARQUET, AVG_FILE_LENGTH, Long.parseLong("32768")); + nonParquetMetrics.addMeanMetricValue(NON_PARQUET, AVG_READ_LEN_REQUESTED, Long.parseLong("32769")); + nonParquetMetrics.addMeanMetricValue(NON_PARQUET, AVG_SIZE_READ_BY_FIRST_READ, Long.parseLong("16384")); + nonParquetMetrics.addMeanMetricValue(NON_PARQUET, AVG_OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ, 1); + nonParquetMetrics.addMeanMetricValue(NON_PARQUET, AVG_FIRST_OFFSET_DIFF, Long.parseLong("16384")); + nonParquetMetrics.addMeanMetricValue(NON_PARQUET, AVG_SECOND_OFFSET_DIFF, Long.parseLong("16384")); nonParquetMetrics.incrementMetricValue(NON_PARQUET, TOTAL_FILES); return nonParquetMetrics; } @@ -222,10 +220,10 @@ private AbfsReadFooterMetrics getNonParquetMetrics() { */ private AbfsReadFooterMetrics getParquetMetrics() { AbfsReadFooterMetrics parquetMetrics = new AbfsReadFooterMetrics(); - parquetMetrics.updateMetricValue(PARQUET, FILE_LENGTH, Long.parseLong("8388608")); - parquetMetrics.updateMetricValue(PARQUET, READ_LEN_REQUESTED, Long.parseLong("8388608")); - parquetMetrics.updateMetricValue(PARQUET, SIZE_READ_BY_FIRST_READ, Long.parseLong("1024")); - parquetMetrics.updateMetricValue(PARQUET, OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ, Long.parseLong("4096")); + parquetMetrics.addMeanMetricValue(PARQUET, AVG_FILE_LENGTH, Long.parseLong("8388608")); + parquetMetrics.addMeanMetricValue(PARQUET, AVG_READ_LEN_REQUESTED, Long.parseLong("8388608")); + parquetMetrics.addMeanMetricValue(PARQUET, AVG_SIZE_READ_BY_FIRST_READ, Long.parseLong("1024")); + parquetMetrics.addMeanMetricValue(PARQUET, AVG_OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ, Long.parseLong("4096")); parquetMetrics.incrementMetricValue(PARQUET, TOTAL_FILES); return parquetMetrics; } diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java index 8161cbb353cc2..686b17baa9149 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java @@ -47,9 +47,6 @@ public void metricsUpdateForFirstRead() { Assertions.assertThat(metrics.getTotalFiles()) .describedAs("Total number of files") .isEqualTo(0); - Assertions.assertThat(metrics.getTotalReadCount()) - .describedAs("Total number of read count") - .isEqualTo(0); } /** @@ -62,9 +59,6 @@ public void metricsUpdateForSecondRead() { Assertions.assertThat(metrics.getTotalFiles()) .describedAs("Total number of files") .isEqualTo(1); - Assertions.assertThat(metrics.getTotalReadCount()) - .describedAs("Total number of read count") - .isEqualTo(0); } /** @@ -78,9 +72,6 @@ public void metricsUpdateForOneFile() { Assertions.assertThat(metrics.getTotalFiles()) .describedAs("Total number of files") .isEqualTo(1); - Assertions.assertThat(metrics.getTotalReadCount()) - .describedAs("Total number of read count") - .isEqualTo(1); Assertions.assertThat(metrics.toString()) .describedAs("Metrics after reading 3 reads of the same file") .isEqualTo("$NON_PARQUET:$FR=10000.000_20000.000$SR=10000.000_10000.000$FL=50000.000$RL=30000.000"); @@ -100,9 +91,6 @@ public void metricsUpdateForMultipleFiles() { Assertions.assertThat(metrics.getTotalFiles()) .describedAs("Total number of files") .isEqualTo(2); - Assertions.assertThat(metrics.getTotalReadCount()) - .describedAs("Total number of read count") - .isEqualTo(2); Assertions.assertThat(metrics.toString()) .describedAs("Metrics after reading 3 reads of the same file") .isEqualTo("$NON_PARQUET:$FR=10000.000_12500.000$SR=10000.000_10000.000$FL=37500.000$RL=30000.000"); From 7d80bd2f5ae9fc2dbb150b7e20ed733a86ec9157 Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Tue, 10 Dec 2024 00:20:04 -0800 Subject: [PATCH 24/32] Rename update read method name --- .../fs/azurebfs/services/AbfsInputStream.java | 2 +- .../services/AbfsReadFooterMetrics.java | 2 +- .../services/TestAbfsReadFooterMetrics.java | 24 +++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsInputStream.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsInputStream.java index cacd3b092eb3f..2922365e1a751 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsInputStream.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsInputStream.java @@ -258,7 +258,7 @@ public synchronized int read(final byte[] b, final int off, final int len) throw // There maybe case that we read less than requested data. long filePosAtStartOfBuffer = fCursor - limit; if (abfsReadFooterMetrics != null) { - abfsReadFooterMetrics.checkMetricUpdate(filePathIdentifier, len, contentLength, nextReadPos); + abfsReadFooterMetrics.updateReadMetrics(filePathIdentifier, len, contentLength, nextReadPos); } if (nextReadPos >= filePosAtStartOfBuffer && nextReadPos <= fCursor) { // Determining position in buffer from where data is to be read. diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java index e83657a7f74a6..a4d9968094835 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java @@ -230,7 +230,7 @@ public void updateMap(String filePathIdentifier) { * @param contentLength the total content length of the file * @param nextReadPos the position of the next read */ - public void checkMetricUpdate(final String filePathIdentifier, final int len, final long contentLength, final long nextReadPos) { + public void updateReadMetrics(final String filePathIdentifier, final int len, final long contentLength, final long nextReadPos) { FileTypeMetrics fileTypeMetrics = fileTypeMetricsMap.computeIfAbsent(filePathIdentifier, key -> new FileTypeMetrics()); if (fileTypeMetrics.getReadCount() == 0 || (fileTypeMetrics.getReadCount() >= 1 && fileTypeMetrics.getCollectMetrics())) { updateMetrics(fileTypeMetrics, len, contentLength, nextReadPos); diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java index 686b17baa9149..d5fd27d258429 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java @@ -43,7 +43,7 @@ public void setUp() { */ @Test public void metricsUpdateForFirstRead() { - metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS); + metrics.updateReadMetrics(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS); Assertions.assertThat(metrics.getTotalFiles()) .describedAs("Total number of files") .isEqualTo(0); @@ -54,8 +54,8 @@ public void metricsUpdateForFirstRead() { */ @Test public void metricsUpdateForSecondRead() { - metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS); - metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS+LENGTH); + metrics.updateReadMetrics(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS); + metrics.updateReadMetrics(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS+LENGTH); Assertions.assertThat(metrics.getTotalFiles()) .describedAs("Total number of files") .isEqualTo(1); @@ -66,9 +66,9 @@ public void metricsUpdateForSecondRead() { */ @Test public void metricsUpdateForOneFile() { - metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS); - metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS+LENGTH); - metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS+2*LENGTH); + metrics.updateReadMetrics(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS); + metrics.updateReadMetrics(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS+LENGTH); + metrics.updateReadMetrics(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS+2*LENGTH); Assertions.assertThat(metrics.getTotalFiles()) .describedAs("Total number of files") .isEqualTo(1); @@ -82,12 +82,12 @@ public void metricsUpdateForOneFile() { */ @Test public void metricsUpdateForMultipleFiles() { - metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS); - metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS+LENGTH); - metrics.checkMetricUpdate(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS+2*LENGTH); - metrics.checkMetricUpdate(TEST_FILE2, LENGTH, CONTENT_LENGTH/2, NEXT_READ_POS); - metrics.checkMetricUpdate(TEST_FILE2, LENGTH, CONTENT_LENGTH/2, NEXT_READ_POS+LENGTH); - metrics.checkMetricUpdate(TEST_FILE2, LENGTH, CONTENT_LENGTH/2, NEXT_READ_POS+2*LENGTH); + metrics.updateReadMetrics(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS); + metrics.updateReadMetrics(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS+LENGTH); + metrics.updateReadMetrics(TEST_FILE1, LENGTH, CONTENT_LENGTH, NEXT_READ_POS+2*LENGTH); + metrics.updateReadMetrics(TEST_FILE2, LENGTH, CONTENT_LENGTH/2, NEXT_READ_POS); + metrics.updateReadMetrics(TEST_FILE2, LENGTH, CONTENT_LENGTH/2, NEXT_READ_POS+LENGTH); + metrics.updateReadMetrics(TEST_FILE2, LENGTH, CONTENT_LENGTH/2, NEXT_READ_POS+2*LENGTH); Assertions.assertThat(metrics.getTotalFiles()) .describedAs("Total number of files") .isEqualTo(2); From 4feaac7a95afe58d6305041ac763c5f9da98ed5e Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Tue, 10 Dec 2024 04:08:15 -0800 Subject: [PATCH 25/32] Fixed test cases and checkstyle failures --- .../hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java | 3 ++- .../hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java | 3 +-- .../hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java | 2 +- .../fs/azurebfs/services/TestAbfsReadFooterMetrics.java | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java index e14b66189ccec..6e74864260e92 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java @@ -29,7 +29,8 @@ public enum AbfsReadFooterMetricsEnum { TOTAL_FILES("totalFiles", "Total files read", FILE, TYPE_COUNTER), AVG_FILE_LENGTH("avgFileLength", "Average File length", FILE, TYPE_MEAN), AVG_SIZE_READ_BY_FIRST_READ("avgSizeReadByFirstRead", "Average Size read by first read", FILE, TYPE_MEAN), - AVG_OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ("avgOffsetDiffBetweenFirstAndSecondRead", "Average Offset difference between first and second read", FILE, TYPE_MEAN), + AVG_OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ("avgOffsetDiffBetweenFirstAndSecondRead", + "Average Offset difference between first and second read", FILE, TYPE_MEAN), AVG_READ_LEN_REQUESTED("avgReadLenRequested", "Average Read length requested", FILE, TYPE_MEAN), AVG_FIRST_OFFSET_DIFF("avgFirstOffsetDiff", "Average First offset difference", FILE, TYPE_MEAN), AVG_SECOND_OFFSET_DIFF("avgSecondOffsetDiff", "Average Second offset difference", FILE, TYPE_MEAN); diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java index d60e54164c940..5cdc9dd668909 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java @@ -30,7 +30,6 @@ import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.EMPTY_STRING; -import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.HUNDRED; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.THOUSAND; import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.RETRY; import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.COLON; @@ -245,7 +244,7 @@ private void getBaseMetrics(StringBuilder metricBuilder) { metricBuilder.append("$BWT=").append(getMetricValue(NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS)) .append("$IT=").append(getMetricValue(NUMBER_OF_IOPS_THROTTLED_REQUESTS)) .append("$OT=").append(getMetricValue(NUMBER_OF_OTHER_THROTTLED_REQUESTS)) - .append("$RT=").append(formatPercent(totalRequestsThrottled/ (double)getMetricValue(TOTAL_NUMBER_OF_REQUESTS), 3)) + .append("$RT=").append(formatPercent(totalRequestsThrottled/ (double) getMetricValue(TOTAL_NUMBER_OF_REQUESTS), 3)) .append("$NFR=").append(getMetricValue(NUMBER_OF_NETWORK_FAILED_REQUESTS)) .append("$TRNR=").append(getMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING)) .append("$TRF=").append(getMetricValue(NUMBER_OF_REQUESTS_FAILED)) diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java index a4d9968094835..c85931d2033f1 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java @@ -201,7 +201,7 @@ public void incrementMetricValue(FileType fileType, AbfsReadFooterMetricsEnum me * @param value the new value of the metric */ public void addMeanMetricValue(FileType fileType, AbfsReadFooterMetricsEnum metricName, long value) { - addMeanStatistic(fileType + COLON + metricName, value); + addMeanStatistic(fileType + COLON + metricName.getName(), value); } /** diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java index d5fd27d258429..2f5a45555b945 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsReadFooterMetrics.java @@ -74,7 +74,7 @@ public void metricsUpdateForOneFile() { .isEqualTo(1); Assertions.assertThat(metrics.toString()) .describedAs("Metrics after reading 3 reads of the same file") - .isEqualTo("$NON_PARQUET:$FR=10000.000_20000.000$SR=10000.000_10000.000$FL=50000.000$RL=30000.000"); + .isEqualTo("$NON_PARQUET:$FR=10000.000_20000.000$SR=10000.000_10000.000$FL=50000.000$RL=10000.000"); } /** @@ -93,6 +93,6 @@ public void metricsUpdateForMultipleFiles() { .isEqualTo(2); Assertions.assertThat(metrics.toString()) .describedAs("Metrics after reading 3 reads of the same file") - .isEqualTo("$NON_PARQUET:$FR=10000.000_12500.000$SR=10000.000_10000.000$FL=37500.000$RL=30000.000"); + .isEqualTo("$NON_PARQUET:$FR=10000.000_12500.000$SR=10000.000_10000.000$FL=37500.000$RL=10000.000"); } } From bc6f9cbbdb5c2a0226b808d0705968a1d4cd2faa Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Tue, 10 Dec 2024 04:47:10 -0800 Subject: [PATCH 26/32] Read Length correction in test case --- .../apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java index dac5e02a77a40..ad4b0b1049d6d 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadFooterMetrics.java @@ -206,7 +206,7 @@ public void testReadFooterMetrics() throws Exception { private AbfsReadFooterMetrics getNonParquetMetrics() { AbfsReadFooterMetrics nonParquetMetrics = new AbfsReadFooterMetrics(); nonParquetMetrics.addMeanMetricValue(NON_PARQUET, AVG_FILE_LENGTH, Long.parseLong("32768")); - nonParquetMetrics.addMeanMetricValue(NON_PARQUET, AVG_READ_LEN_REQUESTED, Long.parseLong("32769")); + nonParquetMetrics.addMeanMetricValue(NON_PARQUET, AVG_READ_LEN_REQUESTED, Long.parseLong("10923")); nonParquetMetrics.addMeanMetricValue(NON_PARQUET, AVG_SIZE_READ_BY_FIRST_READ, Long.parseLong("16384")); nonParquetMetrics.addMeanMetricValue(NON_PARQUET, AVG_OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ, 1); nonParquetMetrics.addMeanMetricValue(NON_PARQUET, AVG_FIRST_OFFSET_DIFF, Long.parseLong("16384")); @@ -221,7 +221,7 @@ private AbfsReadFooterMetrics getNonParquetMetrics() { private AbfsReadFooterMetrics getParquetMetrics() { AbfsReadFooterMetrics parquetMetrics = new AbfsReadFooterMetrics(); parquetMetrics.addMeanMetricValue(PARQUET, AVG_FILE_LENGTH, Long.parseLong("8388608")); - parquetMetrics.addMeanMetricValue(PARQUET, AVG_READ_LEN_REQUESTED, Long.parseLong("8388608")); + parquetMetrics.addMeanMetricValue(PARQUET, AVG_READ_LEN_REQUESTED, Long.parseLong("2560")); parquetMetrics.addMeanMetricValue(PARQUET, AVG_SIZE_READ_BY_FIRST_READ, Long.parseLong("1024")); parquetMetrics.addMeanMetricValue(PARQUET, AVG_OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ, Long.parseLong("4096")); parquetMetrics.incrementMetricValue(PARQUET, TOTAL_FILES); From 98c8716518cdd13c053b6bf125fb02a5804c7fd6 Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Wed, 11 Dec 2024 04:40:58 -0800 Subject: [PATCH 27/32] Added metrics config in account template --- .../accountName_settings.xml.template | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/hadoop-tools/hadoop-azure/src/test/resources/accountSettings/accountName_settings.xml.template b/hadoop-tools/hadoop-azure/src/test/resources/accountSettings/accountName_settings.xml.template index a7a04655b8134..1fb6880d35eab 100644 --- a/hadoop-tools/hadoop-azure/src/test/resources/accountSettings/accountName_settings.xml.template +++ b/hadoop-tools/hadoop-azure/src/test/resources/accountSettings/accountName_settings.xml.template @@ -50,6 +50,11 @@ 14. READER_RBAC_USER_CLIENT_ID -> readerRBACUser Service principal's client ID 15. READER_RBAC_USER_CLIENT_SECRET -> readerRBACUser Service principal's client secret + ## METRIC SETTINGS ## + 16. METRIC_ACCOUNT_NAME -> to metric account name without domain + 17. METRIC_ACCOUNT_KEY -> Metric account access key + 18. METRIC_CONTAINER -> name of an metric container + 19. METRIC_FORMAT -> format of the metric (INTERNAL_BACKOFF_METRIC_FORMAT, INTERNAL_FOOTER_METRIC_FORMAT, INTERNAL_METRIC_FORMAT) --> @@ -187,4 +192,21 @@ fs.azure.account.oauth2.reader.client.secret READER_RBAC_USER_CLIENT_ID + + + fs.azure.metric.account.name + METRIC_ACCOUNT_NAME.dfs.core.windows.net + + + fs.azure.metric.account.key + METRIC_ACCOUNT_KEY + + + fs.azure.metric.uri + https://METRIC_ACCOUNT_NAME.dfs.core.windows.net/METRIC_CONTAINER + + + fs.azure.metric.format + METRIC_FORMAT + From bd4b1315eaccc08481e62c0fb036738cee805a9e Mon Sep 17 00:00:00 2001 From: manishbhatt Date: Tue, 17 Dec 2024 02:55:15 -0800 Subject: [PATCH 28/32] Move constant to metrics constant file --- .../azurebfs/constants/MetricsConstants.java | 85 ++++++++- .../enums/AbfsBackoffMetricsEnum.java | 26 ++- .../enums/AbfsReadFooterMetricsEnum.java | 8 +- .../hadoop/fs/azurebfs/enums/FileType.java | 4 + .../hadoop/fs/azurebfs/enums/RetryValue.java | 1 + .../azurebfs/services/AbfsBackoffMetrics.java | 101 +++++++---- .../services/AbfsReadFooterMetrics.java | 165 ++++++++++++++++-- .../AbstractAbfsStatisticsSource.java | 14 +- .../fs/azurebfs/statistics/package-info.java | 23 --- 9 files changed, 341 insertions(+), 86 deletions(-) rename hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/{statistics => services}/AbstractAbfsStatisticsSource.java (91%) delete mode 100644 hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/package-info.java diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java index 913d5345397f7..71d314a7d5e65 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/MetricsConstants.java @@ -25,11 +25,94 @@ */ @InterfaceAudience.Private public final class MetricsConstants { - public static final String COLON = ":"; + /** + * Type of ABFS Backoff Metrics: Base or Retry + */ public static final String RETRY = "RETRY"; + /** + * Type of ABFS Backoff Metrics: Base or Retry + */ public static final String BASE = "BASE"; + /** + * Type of ABFS Readfooter Metrics: File + */ public static final String FILE = "FILE"; + /** + * Precision Format for double data type + */ public static final String DOUBLE_PRECISION_FORMAT = "%.3f"; + /** + * Request count that succeeded in x retries + */ + public static final String REQUEST_COUNT = "$RCTSI$_"; + /** + * Min Max Average (This refers to the backoff or sleep time between 2 requests) + */ + public static final String MIN_MAX_AVERAGE = "$MMA$_"; + /** + * Time unit: Seconds + */ + public static final String SECONDS = "s"; + /** + * Number of requests with x retries + */ + public static final String REQUESTS = "R="; + /** + * Number of Bandwidth throttled requests + */ + public static final String BANDWIDTH_THROTTLED_REQUESTS = "$BWT="; + /** + * Number of IOPS throttled requests + */ + public static final String IOPS_THROTTLED_REQUESTS = "$IT="; + /** + * Number of Other throttled requests + */ + public static final String OTHER_THROTTLED_REQUESTS = "$OT="; + /** + * Percentage of requests that are throttled + */ + public static final String PERCENTAGE_THROTTLED_REQUESTS = "$RT="; + /** + * Number of requests which failed due to network errors + */ + public static final String NETWORK_ERROR_REQUESTS = "$NFR="; + /** + * Total number of requests which succeeded without retrying + */ + public static final String SUCCESS_REQUESTS_WITHOUT_RETRY = "$TRNR="; + /** + * Total number of requests which failed + */ + public static final String FAILED_REQUESTS = "$TRF="; + /** + * Total number of requests which were made + */ + public static final String TOTAL_REQUESTS_COUNT = "$TR="; + /** + * Max retry count across all requests + */ + public static final String MAX_RETRY = "$MRC="; + /** + * Special character: Dollar + */ + public static final String CHAR_DOLLAR = "$"; + /** + * String to represent the average first read + */ + public static final String FIRST_READ = ":$FR="; + /** + * String to represent the average second read + */ + public static final String SECOND_READ = "$SR="; + /** + * String to represent the average file length + */ + public static final String FILE_LENGTH = "$FL="; + /** + * String to represent the average read length + */ + public static final String READ_LENGTH = "$RL="; // Private constructor to prevent instantiation private MetricsConstants() { diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java index 3c2454414967f..0f8bae281dfa3 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsBackoffMetricsEnum.java @@ -27,16 +27,23 @@ * Enum representing various ABFS backoff metrics */ public enum AbfsBackoffMetricsEnum { - NUMBER_OF_IOPS_THROTTLED_REQUESTS("numberOfIOPSThrottledRequests", "Number of IOPS throttled requests", BASE, TYPE_COUNTER), - NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS("numberOfBandwidthThrottledRequests", "Number of bandwidth throttled requests", BASE, TYPE_COUNTER), - NUMBER_OF_OTHER_THROTTLED_REQUESTS("numberOfOtherThrottledRequests", "Number of other throttled requests", BASE, TYPE_COUNTER), - NUMBER_OF_NETWORK_FAILED_REQUESTS("numberOfNetworkFailedRequests", "Number of network failed requests", BASE, TYPE_COUNTER), + NUMBER_OF_IOPS_THROTTLED_REQUESTS("numberOfIOPSThrottledRequests", + "Number of IOPS throttled requests", BASE, TYPE_COUNTER), + NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS("numberOfBandwidthThrottledRequests", + "Number of bandwidth throttled requests", BASE, TYPE_COUNTER), + NUMBER_OF_OTHER_THROTTLED_REQUESTS("numberOfOtherThrottledRequests", + "Number of other throttled requests", BASE, TYPE_COUNTER), + NUMBER_OF_NETWORK_FAILED_REQUESTS("numberOfNetworkFailedRequests", + "Number of network failed requests", BASE, TYPE_COUNTER), MAX_RETRY_COUNT("maxRetryCount", "Max retry count", BASE, TYPE_COUNTER), - TOTAL_NUMBER_OF_REQUESTS("totalNumberOfRequests", "Total number of requests", BASE, TYPE_COUNTER), + TOTAL_NUMBER_OF_REQUESTS("totalNumberOfRequests", + "Total number of requests", BASE, TYPE_COUNTER), NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING("numberOfRequestsSucceededWithoutRetrying", "Number of requests succeeded without retrying", BASE, TYPE_COUNTER), - NUMBER_OF_REQUESTS_FAILED("numberOfRequestsFailed", "Number of requests failed", BASE, TYPE_COUNTER), - NUMBER_OF_REQUESTS_SUCCEEDED("numberOfRequestsSucceeded", "Number of requests succeeded", RETRY, TYPE_COUNTER), + NUMBER_OF_REQUESTS_FAILED("numberOfRequestsFailed", + "Number of requests failed", BASE, TYPE_COUNTER), + NUMBER_OF_REQUESTS_SUCCEEDED("numberOfRequestsSucceeded", + "Number of requests succeeded", RETRY, TYPE_COUNTER), MIN_BACK_OFF("minBackOff", "Minimum backoff", RETRY, TYPE_GAUGE), MAX_BACK_OFF("maxBackOff", "Maximum backoff", RETRY, TYPE_GAUGE), TOTAL_BACK_OFF("totalBackoff", "Total backoff", RETRY, TYPE_GAUGE), @@ -55,7 +62,10 @@ public enum AbfsBackoffMetricsEnum { * @param type the type of the metric (BASE or RETRY) * @param statisticType the statistic type of the metric (counter or gauge) */ - AbfsBackoffMetricsEnum(String name, String description, String type, StatisticTypeEnum statisticType) { + AbfsBackoffMetricsEnum(String name, + String description, + String type, + StatisticTypeEnum statisticType) { this.name = name; this.description = description; this.type = type; diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java index 6e74864260e92..51a03d3c883f9 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/AbfsReadFooterMetricsEnum.java @@ -28,7 +28,8 @@ public enum AbfsReadFooterMetricsEnum { TOTAL_FILES("totalFiles", "Total files read", FILE, TYPE_COUNTER), AVG_FILE_LENGTH("avgFileLength", "Average File length", FILE, TYPE_MEAN), - AVG_SIZE_READ_BY_FIRST_READ("avgSizeReadByFirstRead", "Average Size read by first read", FILE, TYPE_MEAN), + AVG_SIZE_READ_BY_FIRST_READ("avgSizeReadByFirstRead", + "Average Size read by first read", FILE, TYPE_MEAN), AVG_OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ("avgOffsetDiffBetweenFirstAndSecondRead", "Average Offset difference between first and second read", FILE, TYPE_MEAN), AVG_READ_LEN_REQUESTED("avgReadLenRequested", "Average Read length requested", FILE, TYPE_MEAN), @@ -48,7 +49,10 @@ public enum AbfsReadFooterMetricsEnum { * @param type the type of the metric (FILE) * @param statisticType the statistic type of the metric (counter or gauge) */ - AbfsReadFooterMetricsEnum(String name, String description, String type, StatisticTypeEnum statisticType) { + AbfsReadFooterMetricsEnum(String name, + String description, + String type, + StatisticTypeEnum statisticType) { this.name = name; this.description = description; this.type = type; diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/FileType.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/FileType.java index f9d03823af3ec..e65a669c32b52 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/FileType.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/FileType.java @@ -18,6 +18,10 @@ package org.apache.hadoop.fs.azurebfs.enums; +/** + * Enum for file types. + * Used in {@link org.apache.hadoop.fs.azurebfs.services.AbfsReadFooterMetrics} to store metrics based on file type. + */ public enum FileType { /** * Parquet file. diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java index ecee0e0f2fd71..244192035409b 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java @@ -20,6 +20,7 @@ /** * Enum for retry values. + * Used in {@link org.apache.hadoop.fs.azurebfs.services.AbfsBackoffMetrics} to store metrics based on the retry count. */ public enum RetryValue { ONE("1"), diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java index 5cdc9dd668909..4469647d06131 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java @@ -26,14 +26,27 @@ import org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum; import org.apache.hadoop.fs.azurebfs.enums.RetryValue; import org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum; -import org.apache.hadoop.fs.azurebfs.statistics.AbstractAbfsStatisticsSource; import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.EMPTY_STRING; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.COLON; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.EQUAL; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.THOUSAND; -import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.RETRY; -import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.COLON; import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.DOUBLE_PRECISION_FORMAT; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.RETRY; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.REQUESTS; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.REQUEST_COUNT; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.SECONDS; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.MIN_MAX_AVERAGE; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.BANDWIDTH_THROTTLED_REQUESTS; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.IOPS_THROTTLED_REQUESTS; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.OTHER_THROTTLED_REQUESTS; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.PERCENTAGE_THROTTLED_REQUESTS; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.NETWORK_ERROR_REQUESTS; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.SUCCESS_REQUESTS_WITHOUT_RETRY; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.FAILED_REQUESTS; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.TOTAL_REQUESTS_COUNT; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.MAX_RETRY; import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.MAX_BACK_OFF; import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.MIN_BACK_OFF; import static org.apache.hadoop.fs.azurebfs.enums.AbfsBackoffMetricsEnum.MAX_RETRY_COUNT; @@ -66,7 +79,8 @@ * retry operations in Azure Blob File System (ABFS). */ public class AbfsBackoffMetrics extends AbstractAbfsStatisticsSource { - private static final List RETRY_LIST = Arrays.asList(ONE, TWO, THREE, FOUR, FIVE_FIFTEEN, FIFTEEN_TWENTY_FIVE, TWENTY_FIVE_AND_ABOVE); + private static final List RETRY_LIST = Arrays.asList( + ONE, TWO, THREE, FOUR, FIVE_FIFTEEN, FIFTEEN_TWENTY_FIVE, TWENTY_FIVE_AND_ABOVE); /** * Constructor to initialize the IOStatisticsStore with counters and gauges. @@ -200,40 +214,40 @@ public void setMetricValue(AbfsBackoffMetricsEnum metric, long value) { setMetricValue(metric, value, null); } - /* - Acronyms :- - 1.RCTSI :- Request count that succeeded in x retries - 2.MMA :- Min Max Average (This refers to the backoff or sleep time between 2 requests) - 3.s :- seconds + /** + * Retrieves the retry metrics. + * + * @param metricBuilder the string builder to append the metrics */ private void getRetryMetrics(StringBuilder metricBuilder) { for (RetryValue retryCount : RETRY_LIST) { long totalRequests = getMetricValue(TOTAL_REQUESTS, retryCount); - metricBuilder.append("$RCTSI$_").append(retryCount.getValue()) - .append("R=").append(getMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED, retryCount)); + metricBuilder.append(REQUEST_COUNT) + .append(retryCount.getValue()) + .append(REQUESTS) + .append(getMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED, retryCount)); if (totalRequests > 0) { - metricBuilder.append("$MMA$_").append(retryCount.getValue()) - .append("R=").append(format(DOUBLE_PRECISION_FORMAT, (double) getMetricValue(MIN_BACK_OFF, retryCount) / THOUSAND)).append("s") - .append(format(DOUBLE_PRECISION_FORMAT, (double) getMetricValue(MAX_BACK_OFF, retryCount) / THOUSAND)).append("s") - .append(format(DOUBLE_PRECISION_FORMAT, ((double) getMetricValue(TOTAL_BACK_OFF, retryCount) / totalRequests) / THOUSAND)).append("s"); + metricBuilder.append(MIN_MAX_AVERAGE) + .append(retryCount.getValue()) + .append(REQUESTS).append(format(DOUBLE_PRECISION_FORMAT, (double) getMetricValue(MIN_BACK_OFF, retryCount) / THOUSAND)) + .append(SECONDS) + .append(format(DOUBLE_PRECISION_FORMAT, (double) getMetricValue(MAX_BACK_OFF, retryCount) / THOUSAND)) + .append(SECONDS) + .append(format(DOUBLE_PRECISION_FORMAT, ((double) getMetricValue(TOTAL_BACK_OFF, retryCount) / totalRequests) / THOUSAND)) + .append(SECONDS); } else { - metricBuilder.append("$MMA$_").append(retryCount.getValue()).append("R=0s"); + metricBuilder.append(MIN_MAX_AVERAGE) + .append(retryCount.getValue()) + .append(REQUESTS + EQUAL + 0 + SECONDS); } } } - /* - Acronyms :- - 1.BWT :- Number of Bandwidth throttled requests - 2.IT :- Number of IOPS throttled requests - 3.OT :- Number of Other throttled requests - 4.NFR :- Number of requests which failed due to network errors - 5.%RT :- Percentage of requests that are throttled - 6.TRNR :- Total number of requests which succeeded without retrying - 7.TRF :- Total number of requests which failed - 8.TR :- Total number of requests which were made - 9.MRC :- Max retry count across all requests + /** + * Retrieves the base metrics. + * + * @param metricBuilder the string builder to append the metrics */ private void getBaseMetrics(StringBuilder metricBuilder) { long totalRequestsThrottled = getMetricValue(NUMBER_OF_NETWORK_FAILED_REQUESTS) @@ -241,15 +255,24 @@ private void getBaseMetrics(StringBuilder metricBuilder) { + getMetricValue(NUMBER_OF_OTHER_THROTTLED_REQUESTS) + getMetricValue(NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS); - metricBuilder.append("$BWT=").append(getMetricValue(NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS)) - .append("$IT=").append(getMetricValue(NUMBER_OF_IOPS_THROTTLED_REQUESTS)) - .append("$OT=").append(getMetricValue(NUMBER_OF_OTHER_THROTTLED_REQUESTS)) - .append("$RT=").append(formatPercent(totalRequestsThrottled/ (double) getMetricValue(TOTAL_NUMBER_OF_REQUESTS), 3)) - .append("$NFR=").append(getMetricValue(NUMBER_OF_NETWORK_FAILED_REQUESTS)) - .append("$TRNR=").append(getMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING)) - .append("$TRF=").append(getMetricValue(NUMBER_OF_REQUESTS_FAILED)) - .append("$TR=").append(getMetricValue(TOTAL_NUMBER_OF_REQUESTS)) - .append("$MRC=").append(getMetricValue(MAX_RETRY_COUNT)); + metricBuilder.append(BANDWIDTH_THROTTLED_REQUESTS) + .append(getMetricValue(NUMBER_OF_BANDWIDTH_THROTTLED_REQUESTS)) + .append(IOPS_THROTTLED_REQUESTS) + .append(getMetricValue(NUMBER_OF_IOPS_THROTTLED_REQUESTS)) + .append(OTHER_THROTTLED_REQUESTS) + .append(getMetricValue(NUMBER_OF_OTHER_THROTTLED_REQUESTS)) + .append(PERCENTAGE_THROTTLED_REQUESTS) + .append(formatPercent(totalRequestsThrottled/ (double) getMetricValue(TOTAL_NUMBER_OF_REQUESTS), 3)) + .append(NETWORK_ERROR_REQUESTS) + .append(getMetricValue(NUMBER_OF_NETWORK_FAILED_REQUESTS)) + .append(SUCCESS_REQUESTS_WITHOUT_RETRY) + .append(getMetricValue(NUMBER_OF_REQUESTS_SUCCEEDED_WITHOUT_RETRYING)) + .append(FAILED_REQUESTS) + .append(getMetricValue(NUMBER_OF_REQUESTS_FAILED)) + .append(TOTAL_REQUESTS_COUNT) + .append(getMetricValue(TOTAL_NUMBER_OF_REQUESTS)) + .append(MAX_RETRY) + .append(getMetricValue(MAX_RETRY_COUNT)); } /** @@ -268,6 +291,12 @@ public String toString() { return metricBuilder.toString(); } + /** + * Retrieves the metric names based on the statistic type. + * + * @param type the type of the statistic (counter or gauge) + * @return an array of metric names + */ @VisibleForTesting String[] getMetricNamesByType(StatisticTypeEnum type) { return getMetricNames(type); diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java index c85931d2033f1..62da0f44e5127 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsReadFooterMetrics.java @@ -28,13 +28,18 @@ import org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum; import org.apache.hadoop.fs.azurebfs.enums.FileType; import org.apache.hadoop.fs.azurebfs.enums.StatisticTypeEnum; -import org.apache.hadoop.fs.azurebfs.statistics.AbstractAbfsStatisticsSource; import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.CHAR_UNDERSCORE; +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.COLON; import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.ONE_KB; -import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.FILE; -import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.COLON; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.CHAR_DOLLAR; import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.DOUBLE_PRECISION_FORMAT; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.FILE; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.FIRST_READ; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.SECOND_READ; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.FILE_LENGTH; +import static org.apache.hadoop.fs.azurebfs.constants.MetricsConstants.READ_LENGTH; import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.TOTAL_FILES; import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.AVG_FILE_LENGTH; import static org.apache.hadoop.fs.azurebfs.enums.AbfsReadFooterMetricsEnum.AVG_SIZE_READ_BY_FIRST_READ; @@ -69,6 +74,9 @@ private static final class FileTypeMetrics { private String sizeReadByFirstRead; private String offsetDiffBetweenFirstAndSecondRead; + /** + * Constructor to initialize the file type metrics. + */ private FileTypeMetrics() { collectMetrics = new AtomicBoolean(false); collectMetricsForNextRead = new AtomicBoolean(false); @@ -77,6 +85,9 @@ private FileTypeMetrics() { offsetOfFirstRead = new AtomicLong(0); } + /** + * Updates the file type based on the metrics collected. + */ private void updateFileType() { if (fileType == null) { fileType = collectMetrics.get() && readCount.get() >= 2 @@ -85,68 +96,147 @@ && haveEqualValues(sizeReadByFirstRead) } } + /** + * Checks if the given value has equal parts. + * + * @param value the value to check + * @return true if the value has equal parts, false otherwise + */ private boolean haveEqualValues(String value) { String[] parts = value.split("_"); return parts.length == 2 && parts[0].equals(parts[1]); } + /** + * Increments the read count. + */ private void incrementReadCount() { readCount.incrementAndGet(); } + /** + * Returns the read count. + * + * @return the read count + */ private long getReadCount() { return readCount.get(); } + /** + * Sets the collect metrics flag. + * + * @param collect the value to set + */ private void setCollectMetrics(boolean collect) { collectMetrics.set(collect); } + /** + * Returns the collect metrics flag. + * + * @return the collect metrics flag + */ private boolean getCollectMetrics() { return collectMetrics.get(); } + /** + * Sets the collect metrics for the next read flag. + * + * @param collect the value to set + */ private void setCollectMetricsForNextRead(boolean collect) { collectMetricsForNextRead.set(collect); } + /** + * Returns the collect metrics for the next read flag. + * + * @return the collect metrics for the next read flag + */ private boolean getCollectMetricsForNextRead() { return collectMetricsForNextRead.get(); } + /** + * Returns the collect length metrics flag. + * + * @return the collect length metrics flag + */ private boolean getCollectLenMetrics() { return collectLenMetrics.get(); } + /** + * Sets the collect length metrics flag. + * + * @param collect the value to set + */ private void setCollectLenMetrics(boolean collect) { collectLenMetrics.set(collect); } + /** + * Sets the offset of the first read. + * + * @param offset the value to set + */ private void setOffsetOfFirstRead(long offset) { offsetOfFirstRead.set(offset); } + /** + * Returns the offset of the first read. + * + * @return the offset of the first read + */ private long getOffsetOfFirstRead() { return offsetOfFirstRead.get(); } + /** + * Sets the size read by the first read. + * + * @param size the value to set + */ private void setSizeReadByFirstRead(String size) { sizeReadByFirstRead = size; } + /** + * Returns the size read by the first read. + * + * @return the size read by the first read + */ private String getSizeReadByFirstRead() { return sizeReadByFirstRead; } + /** + * Sets the offset difference between the first and second read. + * + * @param offsetDiff the value to set + */ private void setOffsetDiffBetweenFirstAndSecondRead(String offsetDiff) { offsetDiffBetweenFirstAndSecondRead = offsetDiff; } + /** + * Returns the offset difference between the first and second read. + * + * @return the offset difference between the first and second read + */ private String getOffsetDiffBetweenFirstAndSecondRead() { return offsetDiffBetweenFirstAndSecondRead; } + /** + * Returns the file type. + * + * @return the file type + */ private FileType getFileType() { return fileType; } @@ -165,6 +255,12 @@ public AbfsReadFooterMetrics() { setIOStatistics(ioStatisticsStore); } + /** + * Returns the metric names for a specific statistic type. + * + * @param type the statistic type + * @return the metric names + */ private String[] getMetricNames(StatisticTypeEnum type) { return Arrays.stream(AbfsReadFooterMetricsEnum.values()) .filter(readFooterMetricsEnum -> readFooterMetricsEnum.getStatisticType().equals(type)) @@ -175,10 +271,24 @@ private String[] getMetricNames(StatisticTypeEnum type) { .toArray(String[]::new); } + /** + * Looks up the counter value for a specific metric. + * + * @param fileType the type of the file + * @param metric the metric to look up + * @return the counter value + */ private long getCounterMetricValue(FileType fileType, AbfsReadFooterMetricsEnum metric) { return lookupCounterValue(fileType + COLON + metric.getName()); } + /** + * Looks up the mean statistic value for a specific metric. + * + * @param fileType the type of the file + * @param metric the metric to look up + * @return the mean statistic value + */ private String getMeanMetricValue(FileType fileType, AbfsReadFooterMetricsEnum metric) { return format(DOUBLE_PRECISION_FORMAT, lookupMeanStatistic(fileType + COLON + metric.getName())); } @@ -230,7 +340,10 @@ public void updateMap(String filePathIdentifier) { * @param contentLength the total content length of the file * @param nextReadPos the position of the next read */ - public void updateReadMetrics(final String filePathIdentifier, final int len, final long contentLength, final long nextReadPos) { + public void updateReadMetrics(final String filePathIdentifier, + final int len, + final long contentLength, + final long nextReadPos) { FileTypeMetrics fileTypeMetrics = fileTypeMetricsMap.computeIfAbsent(filePathIdentifier, key -> new FileTypeMetrics()); if (fileTypeMetrics.getReadCount() == 0 || (fileTypeMetrics.getReadCount() >= 1 && fileTypeMetrics.getCollectMetrics())) { updateMetrics(fileTypeMetrics, len, contentLength, nextReadPos); @@ -245,7 +358,10 @@ public void updateReadMetrics(final String filePathIdentifier, final int len, fi * @param contentLength The total content length of the file. * @param nextReadPos The position of the next read operation. */ - private void updateMetrics(FileTypeMetrics fileTypeMetrics, int len, long contentLength, long nextReadPos) { + private void updateMetrics(FileTypeMetrics fileTypeMetrics, + int len, + long contentLength, + long nextReadPos) { synchronized (this) { fileTypeMetrics.incrementReadCount(); } @@ -271,7 +387,10 @@ private void updateMetrics(FileTypeMetrics fileTypeMetrics, int len, long conten * @param len The length of the current read operation. * @param contentLength The total length of the file content. */ - private void handleFirstRead(FileTypeMetrics fileTypeMetrics, long nextReadPos, int len, long contentLength) { + private void handleFirstRead(FileTypeMetrics fileTypeMetrics, + long nextReadPos, + int len, + long contentLength) { if (nextReadPos >= contentLength - (long) Integer.parseInt(FOOTER_LENGTH) * ONE_KB) { fileTypeMetrics.setCollectMetrics(true); fileTypeMetrics.setCollectMetricsForNextRead(true); @@ -290,7 +409,10 @@ private void handleFirstRead(FileTypeMetrics fileTypeMetrics, long nextReadPos, * @param len The length of the current read operation. * @param contentLength The total length of the file content. */ - private void handleSecondRead(FileTypeMetrics fileTypeMetrics, long nextReadPos, int len, long contentLength) { + private void handleSecondRead(FileTypeMetrics fileTypeMetrics, + long nextReadPos, + int len, + long contentLength) { if (fileTypeMetrics.getCollectMetricsForNextRead()) { long offsetDiff = Math.abs(nextReadPos - fileTypeMetrics.getOffsetOfFirstRead()); fileTypeMetrics.setOffsetDiffBetweenFirstAndSecondRead(len + "_" + offsetDiff); @@ -323,7 +445,9 @@ private synchronized void handleFurtherRead(FileTypeMetrics fileTypeMetrics, int * @param len The length of the current read operation. * @param contentLength The total length of the file content. */ - private synchronized void updateMetricsData(FileTypeMetrics fileTypeMetrics, int len, long contentLength) { + private synchronized void updateMetricsData(FileTypeMetrics fileTypeMetrics, + int len, + long contentLength) { long sizeReadByFirstRead = Long.parseLong(fileTypeMetrics.getSizeReadByFirstRead().split("_")[0]); long firstOffsetDiff = Long.parseLong(fileTypeMetrics.getSizeReadByFirstRead().split("_")[1]); long secondOffsetDiff = Long.parseLong(fileTypeMetrics.getOffsetDiffBetweenFirstAndSecondRead().split("_")[1]); @@ -339,6 +463,12 @@ private synchronized void updateMetricsData(FileTypeMetrics fileTypeMetrics, int incrementMetricValue(fileType, TOTAL_FILES); } + /** + * Appends the metrics for a specific file type to the given metric builder. + * + * @param metricBuilder the metric builder to append the metrics to + * @param fileType the file type to append the metrics for + */ private void appendMetrics(StringBuilder metricBuilder, FileType fileType) { long totalFiles = getCounterMetricValue(fileType, TOTAL_FILES); if (totalFiles <= 0) { @@ -349,15 +479,20 @@ private void appendMetrics(StringBuilder metricBuilder, FileType fileType) { String offsetDiffBetweenFirstAndSecondRead = getMeanMetricValue(fileType, AVG_OFFSET_DIFF_BETWEEN_FIRST_AND_SECOND_READ); if (NON_PARQUET.equals(fileType)) { - sizeReadByFirstRead += "_" + getMeanMetricValue(fileType, AVG_FIRST_OFFSET_DIFF); - offsetDiffBetweenFirstAndSecondRead += "_" + getMeanMetricValue(fileType, AVG_SECOND_OFFSET_DIFF); + sizeReadByFirstRead += CHAR_UNDERSCORE + getMeanMetricValue(fileType, AVG_FIRST_OFFSET_DIFF); + offsetDiffBetweenFirstAndSecondRead += CHAR_UNDERSCORE + getMeanMetricValue(fileType, AVG_SECOND_OFFSET_DIFF); } - metricBuilder.append("$").append(fileType) - .append(":$FR=").append(sizeReadByFirstRead) - .append("$SR=").append(offsetDiffBetweenFirstAndSecondRead) - .append("$FL=").append(getMeanMetricValue(fileType, AVG_FILE_LENGTH)) - .append("$RL=").append(getMeanMetricValue(fileType, AVG_READ_LEN_REQUESTED)); + metricBuilder.append(CHAR_DOLLAR) + .append(fileType) + .append(FIRST_READ) + .append(sizeReadByFirstRead) + .append(SECOND_READ) + .append(offsetDiffBetweenFirstAndSecondRead) + .append(FILE_LENGTH) + .append(getMeanMetricValue(fileType, AVG_FILE_LENGTH)) + .append(READ_LENGTH) + .append(getMeanMetricValue(fileType, AVG_READ_LEN_REQUESTED)); } /** diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbstractAbfsStatisticsSource.java similarity index 91% rename from hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java rename to hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbstractAbfsStatisticsSource.java index dfe49e8d8d813..a8f69cf72e2ce 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/AbstractAbfsStatisticsSource.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbstractAbfsStatisticsSource.java @@ -16,7 +16,7 @@ * limitations under the License. */ -package org.apache.hadoop.fs.azurebfs.statistics; +package org.apache.hadoop.fs.azurebfs.services; import org.apache.hadoop.fs.statistics.IOStatistics; import org.apache.hadoop.fs.statistics.IOStatisticsSource; @@ -121,10 +121,22 @@ protected void setGaugeValue(String name, long value) { ioStatisticsStore.setGauge(name, value); } + /** + * Add sample to mean statistics for the given name. + * + * @param name the name of the mean statistic + * @param value the value to set + */ protected void addMeanStatistic(String name, long value) { ioStatisticsStore.addMeanStatisticSample(name, value); } + /** + * Looks up the mean statistics value for the given name. + * + * @param name the name of the mean statistic + * @return the mean value + */ protected double lookupMeanStatistic(String name) { return ioStatisticsStore.meanStatistics().get(name).mean(); } diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/package-info.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/package-info.java deleted file mode 100644 index d9d76e036dbd7..0000000000000 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/statistics/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@InterfaceAudience.Private -@InterfaceStability.Evolving -package org.apache.hadoop.fs.azurebfs.statistics; -import org.apache.hadoop.classification.InterfaceAudience; -import org.apache.hadoop.classification.InterfaceStability; From b9ec4aa576e4619f286d5e6b829b5573829f6b0d Mon Sep 17 00:00:00 2001 From: bhattmanish98 Date: Wed, 18 Dec 2024 02:12:51 -0800 Subject: [PATCH 29/32] Created common method for getPrecisionMetrics --- .../azurebfs/services/AbfsBackoffMetrics.java | 19 ++++++++++++++++--- .../accountName_settings.xml.template | 1 + 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java index 4469647d06131..77844455c035b 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java @@ -214,6 +214,18 @@ public void setMetricValue(AbfsBackoffMetricsEnum metric, long value) { setMetricValue(metric, value, null); } + /** + * Get the precision metrics. + * + * @param metricName the metric name + * @param retryCount the retry count + * @param denominator the denominator + * @return String metrics value with precision + */ + private String getPrecisionMetrics(AbfsBackoffMetricsEnum metricName, RetryValue retryCount, long denominator) { + return format(DOUBLE_PRECISION_FORMAT, (double) getMetricValue(metricName, retryCount) / denominator); + } + /** * Retrieves the retry metrics. * @@ -230,11 +242,12 @@ private void getRetryMetrics(StringBuilder metricBuilder) { if (totalRequests > 0) { metricBuilder.append(MIN_MAX_AVERAGE) .append(retryCount.getValue()) - .append(REQUESTS).append(format(DOUBLE_PRECISION_FORMAT, (double) getMetricValue(MIN_BACK_OFF, retryCount) / THOUSAND)) + .append(REQUESTS) + .append(getPrecisionMetrics(MIN_BACK_OFF, retryCount, THOUSAND)) .append(SECONDS) - .append(format(DOUBLE_PRECISION_FORMAT, (double) getMetricValue(MAX_BACK_OFF, retryCount) / THOUSAND)) + .append(getPrecisionMetrics(MAX_BACK_OFF, retryCount, THOUSAND)) .append(SECONDS) - .append(format(DOUBLE_PRECISION_FORMAT, ((double) getMetricValue(TOTAL_BACK_OFF, retryCount) / totalRequests) / THOUSAND)) + .append(getPrecisionMetrics(TOTAL_BACK_OFF, retryCount, totalRequests * THOUSAND)) .append(SECONDS); } else { metricBuilder.append(MIN_MAX_AVERAGE) diff --git a/hadoop-tools/hadoop-azure/src/test/resources/accountSettings/accountName_settings.xml.template b/hadoop-tools/hadoop-azure/src/test/resources/accountSettings/accountName_settings.xml.template index 1fb6880d35eab..05a6ce5dda7de 100644 --- a/hadoop-tools/hadoop-azure/src/test/resources/accountSettings/accountName_settings.xml.template +++ b/hadoop-tools/hadoop-azure/src/test/resources/accountSettings/accountName_settings.xml.template @@ -193,6 +193,7 @@ READER_RBAC_USER_CLIENT_ID + fs.azure.metric.account.name METRIC_ACCOUNT_NAME.dfs.core.windows.net From 3702356c99d0487c48afe2690630b4a0f5c8199b Mon Sep 17 00:00:00 2001 From: bhattmanish98 Date: Wed, 18 Dec 2024 02:13:35 -0800 Subject: [PATCH 30/32] Created common method for getPrecisionMetrics --- .../hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java index 77844455c035b..b3c10bca2ec44 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsBackoffMetrics.java @@ -222,7 +222,9 @@ public void setMetricValue(AbfsBackoffMetricsEnum metric, long value) { * @param denominator the denominator * @return String metrics value with precision */ - private String getPrecisionMetrics(AbfsBackoffMetricsEnum metricName, RetryValue retryCount, long denominator) { + private String getPrecisionMetrics(AbfsBackoffMetricsEnum metricName, + RetryValue retryCount, + long denominator) { return format(DOUBLE_PRECISION_FORMAT, (double) getMetricValue(metricName, retryCount) / denominator); } From d4f1e09784aa1a07938dcfd7baf61e4b93032f87 Mon Sep 17 00:00:00 2001 From: bhattmanish98 Date: Wed, 18 Dec 2024 03:10:55 -0800 Subject: [PATCH 31/32] Removed entire package name from comments in retryValue and fileType class --- .../java/org/apache/hadoop/fs/azurebfs/enums/FileType.java | 4 +++- .../java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/FileType.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/FileType.java index e65a669c32b52..8a52a5196bddb 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/FileType.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/FileType.java @@ -18,9 +18,11 @@ package org.apache.hadoop.fs.azurebfs.enums; +import org.apache.hadoop.fs.azurebfs.services.AbfsReadFooterMetrics; + /** * Enum for file types. - * Used in {@link org.apache.hadoop.fs.azurebfs.services.AbfsReadFooterMetrics} to store metrics based on file type. + * Used in {@link AbfsReadFooterMetrics} to store metrics based on file type. */ public enum FileType { /** diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java index 244192035409b..f5f40673886e8 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/enums/RetryValue.java @@ -18,9 +18,11 @@ package org.apache.hadoop.fs.azurebfs.enums; +import org.apache.hadoop.fs.azurebfs.services.AbfsBackoffMetrics; + /** * Enum for retry values. - * Used in {@link org.apache.hadoop.fs.azurebfs.services.AbfsBackoffMetrics} to store metrics based on the retry count. + * Used in {@link AbfsBackoffMetrics} to store metrics based on the retry count. */ public enum RetryValue { ONE("1"), From 65b64a94cedbd9f31687ae8e1d913544c30ea414 Mon Sep 17 00:00:00 2001 From: bhattmanish98 Date: Wed, 18 Dec 2024 08:14:23 -0800 Subject: [PATCH 32/32] Add commit to retrigger yetus build --- .../resources/accountSettings/accountName_settings.xml.template | 1 + 1 file changed, 1 insertion(+) diff --git a/hadoop-tools/hadoop-azure/src/test/resources/accountSettings/accountName_settings.xml.template b/hadoop-tools/hadoop-azure/src/test/resources/accountSettings/accountName_settings.xml.template index 05a6ce5dda7de..a31e52497900a 100644 --- a/hadoop-tools/hadoop-azure/src/test/resources/accountSettings/accountName_settings.xml.template +++ b/hadoop-tools/hadoop-azure/src/test/resources/accountSettings/accountName_settings.xml.template @@ -50,6 +50,7 @@ 14. READER_RBAC_USER_CLIENT_ID -> readerRBACUser Service principal's client ID 15. READER_RBAC_USER_CLIENT_SECRET -> readerRBACUser Service principal's client secret + ## METRIC SETTINGS ## 16. METRIC_ACCOUNT_NAME -> to metric account name without domain 17. METRIC_ACCOUNT_KEY -> Metric account access key