@@ -81,6 +81,11 @@ public class EVCacheImpl implements EVCache, EVCacheImplMBean {
8181
8282 private static final Logger log = LoggerFactory .getLogger (EVCacheImpl .class );
8383
84+ // Atomic counter to ensure E flag (recasid) uniqueness within the same millisecond
85+ // Format: timestamp (milliseconds) + sequence number
86+ // This prevents multiple threads from getting identical E flags when calling within the same ms
87+ private static final java .util .concurrent .atomic .AtomicLong recasidSequence = new java .util .concurrent .atomic .AtomicLong (0 );
88+
8489 private final String _appName ;
8590 private final String _cacheName ;
8691 private final String _metricPrefix ;
@@ -3414,10 +3419,61 @@ protected List<Tag> getTags() {
34143419
34153420 // Meta Protocol Operations Implementation
34163421
3422+ /**
3423+ * Generates a unique recasid (E flag) value for CAS synchronization across zones.
3424+ * Uses timestamp + atomic sequence to ensure uniqueness even when called within the same millisecond.
3425+ *
3426+ * Format: (timestamp_ms << 10) | sequence
3427+ * - Upper 54 bits: timestamp in milliseconds (supports ~570 years from epoch)
3428+ * - Lower 10 bits: sequence number (0-1023, wraps every 1024 operations)
3429+ *
3430+ * This allows up to 1024 unique CAS values per millisecond while keeping values compact.
3431+ * Fits within 64-bit CAS token used by memcached.
3432+ */
3433+ private static long generateUniqueRecasid () {
3434+ long timestamp = System .currentTimeMillis ();
3435+ long sequence = recasidSequence .incrementAndGet () & 0x3FF ; // Mask to 10 bits (0-1023)
3436+ return (timestamp << 10 ) | sequence ;
3437+ }
3438+
34173439 @ Override
34183440 public EVCacheLatch metaSet (MetaSetOperation .Builder builder , Policy policy ) throws EVCacheException {
34193441 if (builder == null ) throw new IllegalArgumentException ("Builder cannot be null" );
3420-
3442+
3443+ // Policy enforcement based on operation type:
3444+ // 1. ADD mode (leases/locks) - REQUIRES Policy.ONE to avoid distributed race conditions
3445+ // 2. Regular SET - REQUIRES Policy.ALL for E flag synchronization
3446+ // 3. CAS validation - User chooses (depends on whether they hold a lease)
3447+ boolean isAddMode = builder .build ().getMode () == MetaSetOperation .SetMode .ADD ;
3448+ boolean hasCasValidation = builder .build ().getCas () > 0 ;
3449+
3450+ if (isAddMode && policy != Policy .ONE ) {
3451+ // ADD mode (leases/locks) requires Policy.ONE
3452+ // - Policy.QUORUM can result in 2+ winners (each client gets quorum in different zones)
3453+ // - Policy.ALL results in no winners (distributed race - each succeeds in 1 zone, fails in others)
3454+ // - Policy.ONE guarantees exactly 1 winner (first to any zone wins)
3455+ if (log .isInfoEnabled ()) {
3456+ log .info ("META_SET: ADD mode requires Policy.ONE for proper lease semantics. " +
3457+ "Overriding policy from {} to Policy.ONE for app: {}" ,
3458+ policy , _appName );
3459+ }
3460+ policy = Policy .ONE ;
3461+ } else if (!isAddMode && !hasCasValidation && policy != Policy .ALL ) {
3462+ // Regular SET (no ADD, no CAS) requires Policy.ALL
3463+ // E flag is ALWAYS auto-generated for multi-zone CAS synchronization
3464+ // Therefore, Policy.ALL is REQUIRED to guarantee all zones have the same CAS
3465+ if (log .isInfoEnabled ()) {
3466+ log .info ("META_SET: E flag requires Policy.ALL for CAS synchronization. " +
3467+ "Overriding policy from {} to Policy.ALL for app: {} (mode: {})" ,
3468+ policy , _appName , builder .build ().getMode ());
3469+ }
3470+ policy = Policy .ALL ;
3471+ }
3472+ // CAS validation: No enforcement - user chooses Policy based on whether they hold a lease
3473+ // - WITH lease (mutual exclusion): Use Policy.ALL
3474+ // - WITHOUT lease (competitive): Use Policy.QUORUM
3475+ // We cannot detect if user has lease (it's a different key), so user must choose correctly
3476+
34213477 final boolean throwExc = doThrowException ();
34223478 final EVCacheClient [] clients = _pool .getEVCacheClientForWrite ();
34233479 if (clients .length == 0 ) {
@@ -3447,26 +3503,52 @@ public EVCacheLatch metaSet(MetaSetOperation.Builder builder, Policy policy) thr
34473503
34483504 final long start = EVCacheMetricsFactory .getInstance ().getRegistry ().clock ().wallTime ();
34493505 String status = EVCacheMetricsFactory .SUCCESS ;
3450- final EVCacheLatchImpl latch = new EVCacheLatchImpl (policy == null ? Policy .ALL_MINUS_1 : policy ,
3506+ final EVCacheLatchImpl latch = new EVCacheLatchImpl (policy == null ? Policy .ALL_MINUS_1 : policy ,
34513507 clients .length - _pool .getWriteOnlyEVCacheClients ().length , _appName );
3452-
3508+
3509+ // Auto-generate recasid (E flag) for multi-zone CAS synchronization
3510+ // E flag sets CAS explicitly (requires memcached 1.6.21+ with meta commands)
3511+ // Client ALWAYS generates CAS token to ensure all zones have identical CAS values
3512+ long recasidToUse = builder .build ().getRecasid ();
3513+ if (recasidToUse <= 0 ) {
3514+ // Auto-generate unique timestamp-based CAS token if not explicitly provided
3515+ // Format: (timestamp_ms << 10) | sequence
3516+ // This provides ~1000 unique values per millisecond for concurrent operations
3517+ long timestamp = System .currentTimeMillis ();
3518+ long sequence = recasidSequence .incrementAndGet () & 0x3FF ; // 10 bits (0-1023)
3519+ recasidToUse = (timestamp << 10 ) | sequence ;
3520+ if (log .isDebugEnabled () && shouldLog ()) {
3521+ log .debug ("META_SET: Auto-generated recasid for multi-zone CAS sync: {} (ts={}, seq={}) for key: {}" ,
3522+ recasidToUse , timestamp , sequence , key );
3523+ }
3524+ } else {
3525+ if (log .isDebugEnabled () && shouldLog ()) {
3526+ log .debug ("META_SET: Using explicit recasid for multi-zone CAS sync: {} for key: {}" ,
3527+ recasidToUse , key );
3528+ }
3529+ }
3530+
34533531 try {
34543532 for (EVCacheClient client : clients ) {
34553533 final String canonicalKey = evcKey .getCanonicalKey (client .isDuetClient ());
3456-
3534+
3535+ if (log .isDebugEnabled () && shouldLog ()) {
3536+ log .debug ("META_SET : APP " + _appName + ", key : " + canonicalKey );
3537+ }
3538+
34573539 // Create builder with canonical key for this client
34583540 final MetaSetOperation .Builder clientBuilder = new MetaSetOperation .Builder ()
34593541 .key (canonicalKey )
34603542 .value (builder .build ().getValue ())
34613543 .mode (builder .build ().getMode ())
34623544 .expiration (builder .build ().getExpiration ())
34633545 .cas (builder .build ().getCas ())
3546+ .recasid (recasidToUse ) // Use the SAME recasid for all zones!
34643547 .returnCas (builder .build ().isReturnCas ())
34653548 .returnTtl (builder .build ().isReturnTtl ())
34663549 .markStale (builder .build ().isMarkStale ());
34673550
34683551 final EVCacheOperationFuture <Boolean > future = client .metaSet (clientBuilder , latch );
3469- if (log .isDebugEnabled () && shouldLog ()) log .debug ("META_SET : APP " + _appName + ", key : " + canonicalKey + ", Future : " + future );
34703552 }
34713553 if (event != null ) endEvent (event );
34723554 } catch (Exception ex ) {
@@ -3545,29 +3627,31 @@ public <T> Map<String, EVCacheItem<T>> metaGetBulk(Collection<String> keys, Meta
35453627
35463628 final EVCacheOperationFuture <Map <String , EVCacheItem <Object >>> future = client .metaGetBulk (canonicalConfig );
35473629 final Map <String , EVCacheItem <Object >> canonicalResult = future .get ();
3548-
3630+
35493631 // Convert canonical keys back to original keys and decode values
3550- for ( int i = 0 ; i < keys . size (); i ++) {
3551- final String originalKey = (( ArrayList < String >) keys ). get ( i );
3552- final String canonicalKey = ((ArrayList <String >) canonicalKeys ).get (i );
3553-
3632+ int loopIndex = 0 ;
3633+ for ( String originalKey : keys ) {
3634+ final String canonicalKey = ((ArrayList <String >) canonicalKeys ).get (loopIndex );
3635+
35543636 if (canonicalResult .containsKey (canonicalKey )) {
35553637 final EVCacheItem <Object > canonicalItem = canonicalResult .get (canonicalKey );
35563638 final EVCacheItem <T > item = new EVCacheItem <T >();
3557-
3639+
35583640 // Decode the data using transcoder
35593641 if (canonicalItem .getData () != null && canonicalItem .getData () instanceof CachedData ) {
35603642 final CachedData cd = (CachedData ) canonicalItem .getData ();
3561- final Transcoder <T > transcoder = (tc == null ) ? (Transcoder <T >) _transcoder : tc ;
3643+ // Use same transcoder fallback logic as regular get() method
3644+ final Transcoder <T > transcoder = (tc == null ) ? ((_transcoder == null ) ? (Transcoder <T >) client .getTranscoder () : (Transcoder <T >) _transcoder ) : tc ;
35623645 item .setData (transcoder .decode (cd ));
35633646 } else {
35643647 item .setData ((T ) canonicalItem .getData ());
35653648 }
3566-
3649+
35673650 item .setFlag (canonicalItem .getFlag ());
35683651 item .getItemMetaData ().copyFrom (canonicalItem .getItemMetaData ());
35693652 decanonicalR .put (originalKey , item );
35703653 }
3654+ loopIndex ++;
35713655 }
35723656
35733657 if (event != null ) endEvent (event );
@@ -3599,7 +3683,18 @@ public <T> Map<String, EVCacheItem<T>> metaGetBulk(Transcoder<T> tc, String... k
35993683 @ Override
36003684 public EVCacheLatch metaDelete (MetaDeleteOperation .Builder builder , Policy policy ) throws EVCacheException {
36013685 if (builder == null ) throw new IllegalArgumentException ("Builder cannot be null" );
3602-
3686+
3687+ // E flag is ALWAYS auto-generated for multi-zone CAS synchronization
3688+ // Therefore, Policy.ALL is REQUIRED to guarantee all zones have the same tombstone CAS
3689+ if (policy != Policy .ALL ) {
3690+ if (log .isInfoEnabled ()) {
3691+ log .info ("META_DELETE: E flag requires Policy.ALL for CAS synchronization. " +
3692+ "Overriding policy from {} to Policy.ALL for app: {}" ,
3693+ policy , _appName );
3694+ }
3695+ policy = Policy .ALL ;
3696+ }
3697+
36033698 final boolean throwExc = doThrowException ();
36043699 final EVCacheClient [] clients = _pool .getEVCacheClientForWrite ();
36053700 if (clients .length == 0 ) {
@@ -3629,22 +3724,48 @@ public EVCacheLatch metaDelete(MetaDeleteOperation.Builder builder, Policy polic
36293724
36303725 final long start = EVCacheMetricsFactory .getInstance ().getRegistry ().clock ().wallTime ();
36313726 String status = EVCacheMetricsFactory .SUCCESS ;
3632- final EVCacheLatchImpl latch = new EVCacheLatchImpl (policy == null ? Policy .ALL_MINUS_1 : policy ,
3727+ final EVCacheLatchImpl latch = new EVCacheLatchImpl (policy == null ? Policy .ALL_MINUS_1 : policy ,
36333728 clients .length - _pool .getWriteOnlyEVCacheClients ().length , _appName );
3634-
3729+
3730+ // Auto-generate recasid (E flag) for multi-zone tombstone CAS synchronization
3731+ // E flag sets tombstone CAS explicitly (requires memcached 1.6.21+ with meta commands)
3732+ // Client ALWAYS generates CAS token to ensure all zones have identical tombstone CAS values
3733+ long recasidToUse = builder .build ().getRecasid ();
3734+ if (recasidToUse <= 0 ) {
3735+ // Auto-generate unique timestamp-based CAS token if not explicitly provided
3736+ // Format: (timestamp_ms << 10) | sequence
3737+ // This provides ~1000 unique values per millisecond for concurrent operations
3738+ long timestamp = System .currentTimeMillis ();
3739+ long sequence = recasidSequence .incrementAndGet () & 0x3FF ; // 10 bits (0-1023)
3740+ recasidToUse = (timestamp << 10 ) | sequence ;
3741+ if (log .isDebugEnabled () && shouldLog ()) {
3742+ log .debug ("META_DELETE: Auto-generated recasid for multi-zone tombstone CAS sync: {} (ts={}, seq={}) for key: {}" ,
3743+ recasidToUse , timestamp , sequence , key );
3744+ }
3745+ } else {
3746+ if (log .isDebugEnabled () && shouldLog ()) {
3747+ log .debug ("META_DELETE: Using explicit recasid for multi-zone tombstone CAS sync: {} for key: {}" ,
3748+ recasidToUse , key );
3749+ }
3750+ }
3751+
36353752 try {
36363753 for (EVCacheClient client : clients ) {
36373754 final String canonicalKey = evcKey .getCanonicalKey (client .isDuetClient ());
3638-
3755+
3756+ if (log .isDebugEnabled () && shouldLog ()) {
3757+ log .debug ("META_DELETE : APP " + _appName + ", key : " + canonicalKey );
3758+ }
3759+
36393760 // Create builder with canonical key for this client
36403761 final MetaDeleteOperation .Builder clientBuilder = new MetaDeleteOperation .Builder ()
36413762 .key (canonicalKey )
3642- .mode (builder .getMode ())
3643- .cas (builder .getCas ())
3644- .returnTtl (builder .isReturnTtl ());
3763+ .mode (builder .build ().getMode ())
3764+ .cas (builder .build ().getCas ())
3765+ .recasid (recasidToUse ) // Use the SAME recasid for all zones!
3766+ .returnTtl (builder .build ().isReturnTtl ());
36453767
36463768 final EVCacheOperationFuture <Boolean > future = client .metaDelete (clientBuilder , latch );
3647- if (log .isDebugEnabled () && shouldLog ()) log .debug ("META_DELETE : APP " + _appName + ", key : " + canonicalKey + ", Future : " + future );
36483769 }
36493770 if (event != null ) endEvent (event );
36503771 } catch (Exception ex ) {
0 commit comments