2222
2323package com .netflix .evcache ;
2424
25+ import com .github .luben .zstd .Zstd ;
26+ import com .github .luben .zstd .ZstdInputStream ;
2527import com .netflix .evcache .metrics .EVCacheMetricsFactory ;
26- import com .netflix .evcache .pool .ServerGroup ;
2728import com .netflix .spectator .api .BasicTag ;
2829import com .netflix .spectator .api .Tag ;
29- import com .netflix .spectator .api .Timer ;
3030import net .spy .memcached .CachedData ;
3131import net .spy .memcached .transcoders .BaseSerializingTranscoder ;
3232import net .spy .memcached .transcoders .Transcoder ;
3333import net .spy .memcached .transcoders .TranscoderUtils ;
34- import net .spy .memcached .util .StringUtils ;
3534
36- import java .time .Duration ;
35+ import java .io .ByteArrayInputStream ;
36+ import java .io .ByteArrayOutputStream ;
37+ import java .io .IOException ;
38+ import java .io .InputStream ;
39+ import java .nio .ByteBuffer ;
40+ import java .nio .ByteOrder ;
3741import java .util .ArrayList ;
3842import java .util .Date ;
3943import java .util .List ;
40- import java .util .Map ;
41- import java .util .concurrent .ConcurrentHashMap ;
42- import java .util .concurrent .TimeUnit ;
4344
4445
4546/**
@@ -65,8 +66,15 @@ public class EVCacheSerializingTranscoder extends BaseSerializingTranscoder impl
6566
6667 static final String COMPRESSION = "COMPRESSION_METRIC" ;
6768
69+ public enum CompressionAlgorithm { GZIP , ZSTD }
70+
71+ public static final int DEFAULT_ZSTD_COMPRESSION_LEVEL = 3 ;
72+
73+ private static final int ZSTD_MAGIC = 0xFD2FB528 ;
74+
6875 private final TranscoderUtils tu = new TranscoderUtils (true );
69- private Timer timer ;
76+ private CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm .GZIP ;
77+ private int zstdLevel = DEFAULT_ZSTD_COMPRESSION_LEVEL ;
7078
7179 /**
7280 * Get a serializing transcoder with the default max data size.
@@ -82,6 +90,14 @@ public EVCacheSerializingTranscoder(int max) {
8290 super (max );
8391 }
8492
93+ public void setCompressionAlgorithm (CompressionAlgorithm algo ) {
94+ this .compressionAlgorithm = algo ;
95+ }
96+
97+ public void setCompressionLevel (int level ) {
98+ this .zstdLevel = level ;
99+ }
100+
85101 @ Override
86102 public boolean asyncDecode (CachedData d ) {
87103 if ((d .getFlags () & COMPRESSED ) != 0 || (d .getFlags () & SERIALIZED ) != 0 ) {
@@ -179,31 +195,90 @@ public CachedData encode(Object o) {
179195 }
180196 assert b != null ;
181197 if (b .length > compressionThreshold ) {
198+ int originalLength = b .length ;
182199 byte [] compressed = compress (b );
183- if (compressed .length < b . length ) {
200+ if (compressed .length < originalLength ) {
184201 getLogger ().trace ("Compressed %s from %d to %d" ,
185- o .getClass ().getName (), b . length , compressed .length );
202+ o .getClass ().getName (), originalLength , compressed .length );
186203 b = compressed ;
187204 flags |= COMPRESSED ;
188205 } else {
189206 getLogger ().debug ("Compression increased the size of %s from %d to %d" ,
190- o .getClass ().getName (), b . length , compressed .length );
207+ o .getClass ().getName (), originalLength , compressed .length );
191208 }
192209
193- long compression_ratio = Math .round ((double ) compressed .length / b . length * 100 );
194- updateTimerWithCompressionRatio ( compression_ratio );
210+ long ratioPerCent = Math .round ((double ) compressed .length / originalLength * 100 );
211+ recordCompressionRatio ( ratioPerCent );
195212 }
196213 return new CachedData (flags , b , getMaxSize ());
197214 }
198215
199- private void updateTimerWithCompressionRatio (long ratio_percentage ) {
200- if (timer == null ) {
201- final List <Tag > tagList = new ArrayList <Tag >(1 );
202- tagList .add (new BasicTag (EVCacheMetricsFactory .COMPRESSION_TYPE , "gzip" ));
203- timer = EVCacheMetricsFactory .getInstance ().getPercentileTimer (EVCacheMetricsFactory .COMPRESSION_RATIO , tagList , Duration .ofMillis (100 ));
204- };
216+ @ Override
217+ protected byte [] compress (byte [] in ) {
218+ if (in == null ) throw new NullPointerException ("Can't compress null" );
219+ switch (compressionAlgorithm ) {
220+ case ZSTD :
221+ return Zstd .compress (in , zstdLevel );
222+ case GZIP :
223+ return super .compress (in );
224+ default :
225+ throw new IllegalArgumentException ("Unsupported compression algorithm: " + compressionAlgorithm );
226+ }
227+ }
228+
229+ @ Override
230+ protected byte [] decompress (byte [] in ) {
231+ if (in == null || in .length == 0 ) return in ;
232+ if (isZstdCompressed (in )) return decompressZstd (in );
233+ return super .decompress (in );
234+ }
235+
236+ private boolean isZstdCompressed (byte [] data ) {
237+ if (data == null || data .length < 4 ) return false ;
238+ int magic = ByteBuffer .wrap (data , 0 , 4 ).order (ByteOrder .LITTLE_ENDIAN ).getInt ();
239+ return magic == ZSTD_MAGIC ;
240+ }
241+
242+ private byte [] decompressZstd (byte [] in ) {
243+ long originalSize = Zstd .decompressedSize (in );
244+ if (originalSize > Integer .MAX_VALUE ) {
245+ getLogger ().warn ("Zstd decompressed size exceeds int range: " + originalSize );
246+ return null ;
247+ }
248+ if (originalSize > 0 ) {
249+ // Fast path: frame carries a content-size header (compress() above always does).
250+ return Zstd .decompress (in , (int ) originalSize );
251+ }
252+ // Slow path: declared size is 0, unknown (-1), or invalid (-2) — stream-decode and let
253+ // ZstdInputStream surface any frame errors.
254+ ZstdInputStream zis = null ;
255+ try {
256+ zis = new ZstdInputStream (new ByteArrayInputStream (in ));
257+ return readAll (zis );
258+ } catch (IOException e ) {
259+ getLogger ().error ("Error reading Zstd input stream" , e );
260+ return null ;
261+ } finally {
262+ try { if (zis != null ) zis .close (); } catch (IOException ignored ) {}
263+ }
264+ }
265+
266+ private static byte [] readAll (InputStream in ) throws IOException {
267+ ByteArrayOutputStream out = new ByteArrayOutputStream ();
268+ byte [] buf = new byte [8192 ];
269+ int n ;
270+ while ((n = in .read (buf )) != -1 ) {
271+ out .write (buf , 0 , n );
272+ }
273+ return out .toByteArray ();
274+ }
205275
206- timer .record (ratio_percentage , TimeUnit .MILLISECONDS );
276+ private void recordCompressionRatio (long ratioPerCent ) {
277+ final List <Tag > tagList = new ArrayList <Tag >(1 );
278+ tagList .add (new BasicTag (EVCacheMetricsFactory .COMPRESSION_TYPE , compressionAlgorithm .name ().toLowerCase ()));
279+ EVCacheMetricsFactory .getInstance ()
280+ .getDistributionSummary (EVCacheMetricsFactory .COMPRESSION_RATIO , tagList )
281+ .record (ratioPerCent );
207282 }
208283
209284}
0 commit comments