/* * Hibernate OGM, Domain model persistence for NoSQL datastores * * License: GNU Lesser General Public License (LGPL), version 2.1 or later * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.ogm.datastore.infinispanremote.impl.sequences; import java.util.Random; import org.hibernate.ogm.datastore.infinispanremote.impl.protostream.OgmProtoStreamMarshaller; import org.hibernate.ogm.datastore.infinispanremote.impl.schema.SequenceTableDefinition; import org.hibernate.ogm.datastore.infinispanremote.logging.impl.Log; import org.hibernate.ogm.datastore.infinispanremote.logging.impl.LoggerFactory; import org.hibernate.ogm.dialect.spi.NextValueRequest; import org.infinispan.client.hotrod.RemoteCache; import org.infinispan.client.hotrod.VersionedValue; import org.infinispan.protostream.SerializationContext; /** * This implements the atomic operations to model a source for named sequences. * The SequenceId represents the name of this sequence and is specific to a Remote Cache. * * The implementation uses optimistic locking and is therefore not suited for * high contention scenarios. If an high degree of contention is detected, * a warning will be logged and a randomized exponential backoff strategy * will kick in in hope of resolving the contention problem but severely * impacting performance: if this warning is logged it's best to consider * alternative ID assignment strategies in the domain model; ideally * application assigned. * * Also, the Hot Rod client API currently doesn't allow to issue both the * CAS operation and return the new version in case of failure, so the failure of * an optimistic replace operation needs to re-read the version, introducing * additional delays and making further failures more likely. * * @author Sanne Grinovero */ public final class HotRodSequencer { private static final Log log = LoggerFactory.getLogger(); private static final int START_EXPONENTIAL_FALLBACK = 10; private static final int LOG_WARNING_EACH_N_OPS = 10; private final RemoteCache<SequenceId, Long> remoteCache; private final SerializationContext serContext; private final OgmProtoStreamMarshaller marshaller; private final int increment; private final SequenceId id; private final Random random = new Random(); private long lastKnownVersion = -1; private Long lastKnownRemoteValue = null; HotRodSequencer( RemoteCache<SequenceId, Long> remoteCache, SequenceTableDefinition sequenceTableDefinition, NextValueRequest initialRequest, SerializationContext serContext, OgmProtoStreamMarshaller marshaller) { this.remoteCache = remoteCache; this.increment = initialRequest.getIncrement(); this.serContext = serContext; this.marshaller = marshaller; this.id = new SequenceId( initialRequest.getKey().getColumnValue() ); } Number getSequenceValue(NextValueRequest request) { try { marshaller.setCurrentSerializationContext( serContext ); return getSequenceValueInternal( request ); } finally { marshaller.setCurrentSerializationContext( null ); } } private synchronized Number getSequenceValueInternal(NextValueRequest request) { if ( lastKnownRemoteValue == null ) { Long initialValue = (long) request.getInitialValue(); Long previous = remoteCache.putIfAbsent( id, initialValue ); //Side effects: initialize fields with first known values from remote getRemoteVersion(); if ( previous == null ) { //if the putIfAbsent CAS was successful, we can return already return lastKnownRemoteValue; } } //now to CAS: int casCycle = 0; while ( true ) { Long targetValue = Long.valueOf( lastKnownRemoteValue.longValue() + increment ); boolean done = attemptCASWriteValue( targetValue ); if ( done ) { return targetValue; } else { //On failure of CAS, refresh what we know about the remote version and value: getRemoteVersion(); casCycle++; if ( casCycle % LOG_WARNING_EACH_N_OPS == 0 ) { log.excessiveCasForSequencer( id.getSegmentName() ); } if ( casCycle > START_EXPONENTIAL_FALLBACK ) { delayRandomizerAtCycle( casCycle - START_EXPONENTIAL_FALLBACK ); } } } } private void delayRandomizerAtCycle(int casCycle) { final int exponentialMaxMilliseconds = 2 ^ casCycle; final int nextWait = random.nextInt( exponentialMaxMilliseconds ); if ( nextWait == 0 ) { return; } try { Thread.sleep( nextWait ); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw log.interruptedDuringCASSequenceGeneration(); } } private boolean attemptCASWriteValue(final Long targetValue) { boolean success = remoteCache.replaceWithVersion( id, targetValue, lastKnownVersion ); if ( success ) { lastKnownVersion++; lastKnownRemoteValue = targetValue; } return success; } private void getRemoteVersion() { VersionedValue<Long> versioned = remoteCache.getVersioned( id ); if ( versioned == null ) { throw log.criticalDataLossDetected(); } lastKnownVersion = versioned.getVersion(); lastKnownRemoteValue = (Long) versioned.getValue(); } }