All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.tectonica.gae.GaeMemcacheLock Maven / Gradle / Ivy

Go to download

Set of Java utility classes, all completely independent, to provide lightweight solutions for common situations

There is a newer version: 0.6.1
Show newest version
package com.tectonica.gae;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import com.google.appengine.api.memcache.Expiration;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.memcache.MemcacheService.SetPolicy;
import com.google.appengine.api.memcache.MemcacheServiceFactory;

/**
 * a {@link Lock} implementation for Google App Engine applications, which uses the Memcache service to ensure global locking (i.e. among
 * all the instances of the application). The lock is re-entrant (i.e. can be locked and then re-locked in a nested method by the same
 * thread) and minimizes the amount of calls to the Memcache service by having all threads from a single instance share the Memcache
 * polling loop.
 * 

* Locking with Memcache is not a bullet-proof solution, as unexpected eviction of the cache may result in a situation where one instance * acquires a lock that is in fact taken by another. However, the risk is very minimal especially if using the Dedicated Plan from Google * (see here). Also, a lock entry in Memcache is very unlikely * to be evicted due to LRU considerations, as locks are either short-lived or very frequently updated (in high contention). * * @author Zach Melamed */ public class GaeMemcacheLock implements Lock { private final static MemcacheService mc = MemcacheServiceFactory.getMemcacheService(); private static final int LOCK_AUTO_EXPIRATION_MS = 30000; private static final long SLEEP_BETWEEN_RETRIES_MS = 50L; private final String globalName; private final boolean disposeWhenUnlocked; private final long sleepBetweenRetriesMS; private final ReentrantLock localLock; private ThreadLocal globalHoldCount = new ThreadLocal() { @Override protected Integer initialValue() { return 0; } }; // ////////////////////////////////////////////////////////////////////////////////////////// private AtomicInteger refCount = new AtomicInteger(0); private static Map locks = new ConcurrentHashMap<>(); public static GaeMemcacheLock getLock(String globalName, boolean disposeWhenUnlocked) { return getLock(globalName, disposeWhenUnlocked, false, SLEEP_BETWEEN_RETRIES_MS); } public static GaeMemcacheLock getLock(String globalName, boolean disposeWhenUnlocked, boolean locallyFair, long sleepBetweenRetriesMS) { synchronized (locks) { GaeMemcacheLock lock = locks.get(globalName); if (lock == null) locks.put(globalName, lock = new GaeMemcacheLock(globalName, disposeWhenUnlocked, locallyFair, sleepBetweenRetriesMS)); lock.refCount.incrementAndGet(); return lock; } } public static void disposeLock(String globalName) { synchronized (locks) { GaeMemcacheLock lock = locks.get(globalName); if (lock != null) { if (lock.refCount.decrementAndGet() == 0) locks.remove(globalName); } } } // ////////////////////////////////////////////////////////////////////////////////////////// private GaeMemcacheLock(String globalName, boolean disposeWhenUnlocked, boolean locallyFair, long sleepBetweenRetriesMS) { this.globalName = globalName; this.disposeWhenUnlocked = disposeWhenUnlocked; this.sleepBetweenRetriesMS = sleepBetweenRetriesMS; localLock = new ReentrantLock(locallyFair); } @Override public void lockInterruptibly() throws InterruptedException { localLock.lock(); waitForGlobalLock(null); } @Override public void lock() { try { lockInterruptibly(); } catch (InterruptedException e) { throw new RuntimeException(e); } } @Override public void unlock() { unlockGlobally(); localLock.unlock(); if (disposeWhenUnlocked) disposeLock(globalName); } @Override public boolean tryLock() { if (!localLock.tryLock()) return false; if (!lockGlobally()) { localLock.unlock(); return false; } return true; } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { long timeout = System.currentTimeMillis() + unit.toMillis(time); if (localLock.tryLock(time, unit)) return waitForGlobalLock(timeout); return false; } @Override public Condition newCondition() { throw new UnsupportedOperationException(); } /** * this method blocks execution until it acquires the global lock (or gets interrupted). it does so using an infinite loop which checks * a shared Memcache entry once in every short-while, until it succeeds in acquiring the lock */ private boolean waitForGlobalLock(Long timeout) throws InterruptedException { while (true) { long before = System.currentTimeMillis(); if (lockGlobally()) return true; // don't wait if we got the lock // check for timeout long after = System.currentTimeMillis(); if (timeout != null && after >= timeout.longValue()) return false; // calculate the exact amount of sleep needed before the next retry long roudtripMS = after - before; long actualSleep = Math.max(0L, sleepBetweenRetriesMS - roudtripMS); if (actualSleep > 0L) { if (timeout != null) { // if we sleep as calculated, how much will we overflow beyond the timeout? long overflow = Math.max(0L, after + actualSleep - timeout.longValue()); if ((actualSleep -= overflow) <= 0L) continue; } TimeUnit.MILLISECONDS.sleep(actualSleep); } } } /** * guaranteed to run only after acquiring the local lock, this method attempts to acquire the global lock too. it uses a reference * counting methodology to support reentrancy. */ private boolean lockGlobally() { int holdCount = globalHoldCount.get().intValue(); boolean acquired; if (holdCount > 0) acquired = true; else acquired = mc.put(globalName, "X", Expiration.byDeltaMillis(LOCK_AUTO_EXPIRATION_MS), SetPolicy.ADD_ONLY_IF_NOT_PRESENT); if (acquired) globalHoldCount.set(holdCount + 1); return acquired; } /** * guaranteed to run before releasing the local lock, this releases the global lock first.it uses a reference counting methodology to * support reentrancy */ private void unlockGlobally() { int holdCount = globalHoldCount.get().intValue() - 1; globalHoldCount.set(holdCount); if (holdCount == 0) mc.delete(globalName); } @Override public int hashCode() { return globalName.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; GaeMemcacheLock other = (GaeMemcacheLock) obj; return globalName.equals(other.globalName); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy