/**
* Copyright [2011] [Datasalt Systems S.L.]
*
* Licensed 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 com.datasalt.utils.commons;
import java.util.Iterator;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
/**
* This semaphore's permits expire after a certain fixed time.
* This can be usefull in scenarios where controlling release() is not trivial.
* <p>
* Calling release() has the same effect than on a normal semaphore: immediately releasing a resource.
*
* @author pere
*
*/
public class ExpiringSemaphore {
// The number of permits
int capacity;
// The expiration time for permits
long expire;
// We use a timestamp oracle as a unique-id generator. It generates always-increasing unique ids.
TimestampOracle oracle;
/**
* This flag is updated each time someone blocks on acquire(). If it receives an expired permit, it increments it.
*/
int expiredCount = 0;
/**
* This semaphore is backed by a delayed queue of fixed size.
* There are as many elements in the queue as permits we can grant.
* The idea is that each permit has a fixed expiration time, and that it can be acquired again
* after that time even if nobody called release() for it.
*/
DelayQueue<DelayedElement> queue;
public ExpiringSemaphore(int capacity, long expire) {
this.capacity = capacity;
this.expire = expire;
oracle = new TimestampOracle();
queue = new DelayQueue<DelayedElement>();
for(int i = 0; i < capacity; i++) {
queue.put(new DelayedElement(oracle.getUniqueTimestamp()));
}
}
/**
* This is the elements that are contained inside the delay queue.
* <p>
* Their equals() compare a unique ID.
* <p>
* Their compare() method is consistent with the getDelay() method and only compares timestamps.
*/
public static class DelayedElement implements Delayed {
Long tstamp;
Long uniqueId;
boolean acquired = false;
public DelayedElement(Long id) {
this.uniqueId = id;
tstamp = System.currentTimeMillis();
}
@Override
public int compareTo(Delayed delayed) {
long thisDelay = getDelay(TimeUnit.MILLISECONDS);
long itsDelay = delayed.getDelay(TimeUnit.MILLISECONDS);
if(thisDelay > itsDelay) {
return 1;
} else if(thisDelay < itsDelay) {
return -1;
}
return 0;
}
@Override
public boolean equals(Object obj) {
return uniqueId.equals(((DelayedElement)obj).uniqueId);
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(tstamp - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
}
Object putMonitor = new Object();
/**
* Acquiring means waiting for a permit to be available. Two things may happen:
* <ul>
* <li>A permit may be released by calling release(). This way the permit's timestamp will be reset to 'now' and it will be grabbed.</li>
* <li>A permit may expire. This way the permit will also be grabbed.</li>
* </ul>
* There is a flag "acquired" that is used internally in order to know which permits have been acquired by expiration or by release().
*
* @throws InterruptedException
*/
public void acquire() throws InterruptedException {
DelayedElement elem = queue.take();
synchronized(putMonitor) {
if(elem.acquired) {
expiredCount++;
} else {
elem.acquired = true;
}
elem.tstamp = System.currentTimeMillis() + expire;
queue.put(elem);
}
}
/**
* Release() means granting one more permit. To do that, we introspect the DelayQueue and find one element such that its timestamp
* tells us that it is no yet available. Then, we remove this element from the queue and we reset its timestamp to 'now' so that it
* becomes automatically available, and we insert it in the queue again. All this is atomically performed by blocking on an internal lock.
*/
public void release() {
synchronized(putMonitor) {
DelayedElement elem = null;
Iterator<DelayedElement> it = queue.iterator();
do {
elem = it.next();
} while(it.hasNext() && elem.tstamp <= System.currentTimeMillis());
if(elem.tstamp > System.currentTimeMillis()) { // otherwise we can't release more
queue.remove(elem);
elem.tstamp = System.currentTimeMillis();
elem.acquired = false;
queue.put(elem);
}
}
}
/**
* This method iterates the queue and returns how many permits have its timestamp > 'now'.
*
*
*/
public int usedPermits() {
synchronized(putMonitor) {
int usedPermits = 0;
Iterator<DelayedElement> elem = queue.iterator();
while(elem.hasNext()) {
if(elem.next().tstamp > System.currentTimeMillis()) {
usedPermits++;
}
}
return usedPermits;
}
}
/**
* This method returns the number of permits that where acquired by acquire() that were already expired.
*
*
*/
public int expiredCount() {
return expiredCount;
}
/**
* This method iterates the queue and returns how many permits have its timestamp <= 'now'.
*
*
*/
public int availablePermits() {
synchronized(putMonitor) {
int availablePermits = 0;
Iterator<DelayedElement> elem = queue.iterator();
while(elem.hasNext()) {
if(elem.next().tstamp <= System.currentTimeMillis()) {
availablePermits++;
}
}
return availablePermits;
}
}
/**
* This method iterates the queue and returns how many permits have its timestamp <= 'now' and have been acquired but not released.
* <p>
* In other words, it tells us how many permits we can grab right now so that these permits are also expired.
* <p>
* In order to know the total number of expired permits by a semaphore when we stop using it, the correct formula to use is:
* currentlyExpiredPermits() + expiredCount()
*
*
*/
public int currentlyExpiredPermits() {
synchronized(putMonitor) {
int expiredPermits = 0;
Iterator<DelayedElement> elem = queue.iterator();
while(elem.hasNext()) {
DelayedElement el = elem.next();
if(el.tstamp <= System.currentTimeMillis() && el.acquired) {
expiredPermits++;
}
}
return expiredPermits;
}
}
}