| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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 org.apache.asyncweb.server.util; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| import org.apache.asyncweb.server.util.TimedPermitIssuer; |
| import org.apache.asyncweb.server.util.PermitExpirationListener; |
| import org.apache.asyncweb.server.util.TimedPermit; |
| |
| /** |
| * A <code>TimedPermitIssuer</code> which stores all issued permits in a linked |
| * list. |
| * As a permit is renewed, its lifetime is extended and it is simply moved to the |
| * back of the list (As <code>LinkedPermitIssuer</code> uses a fixed lifetime for |
| * all permits and renewals).<br/> |
| * Each permit issued by this issuer has direct access to its place in the list - |
| * allowing constant time renewals. |
| * |
| * |
| */ |
| public class LinkedPermitIssuer implements TimedPermitIssuer |
| { |
| |
| private static final Logger LOG = LoggerFactory |
| .getLogger(LinkedPermitIssuer.class); |
| |
| /** |
| * The head of the permit list. |
| * This entry is the next due to expire |
| */ |
| private PermitEntry head; |
| |
| /** |
| * The tail of the permit list |
| */ |
| private PermitEntry tail; |
| |
| /** |
| * The lifetime given to new permits, and permit renewals |
| */ |
| private long lifetime; |
| |
| private Object lock = new Object(); |
| |
| private List<PermitExpirationListener> listeners = Collections |
| .synchronizedList(new ArrayList<PermitExpirationListener>()); |
| |
| private boolean isClosed; |
| |
| /** |
| * Creates a <code>LinkedPermitIssuer</code> with a specified lifetime given to |
| * new permits. When a permit issued by this issuer is renewed, its expiry time |
| * is renewed to the current time plus this lifetime |
| * |
| * @param lifetime The lifetime to be used for new permits, and permit renewals |
| */ |
| public LinkedPermitIssuer(long lifetime) { |
| if (lifetime <= 0) { |
| throw new IllegalArgumentException("lifetime must be >0"); |
| } |
| this.lifetime = lifetime; |
| new Thread(new ExpiryNotifier()).start(); |
| } |
| |
| /** |
| * Issues a new <code>TimedPermit</code> for the target object. |
| * Unless <code>renew</code>ed, the permit expires after this |
| * issuers imposed lifetime. Upon renewal, the permit becomes valid |
| * for this issuers configured lifetime from the time of the renewal. |
| * |
| * @param o The target object |
| */ |
| public TimedPermit issuePermit(Object o) { |
| PermitEntry permit; |
| synchronized (lock) { |
| permit = new PermitEntry(o); |
| if (isEmpty()) { |
| head = tail = permit; |
| lock.notify(); // notify when move from empty to non empty |
| } else { |
| tail.entryAfter = permit; |
| permit.entryBefore = tail; |
| tail = permit; |
| } |
| } |
| return permit; |
| } |
| |
| /** |
| * Adds a <code>PermitExpirationListener</code> to this issuer |
| * |
| * @param listener the listener |
| */ |
| public void addPermitExpirationListener(PermitExpirationListener listener) { |
| listeners.add(listener); |
| } |
| |
| /** |
| * Closes this issuer |
| */ |
| public void close() { |
| synchronized (lock) { |
| isClosed = true; |
| lock.notify(); |
| LOG.debug("Marked as closed"); |
| } |
| } |
| |
| /** |
| * Determines whether there are any outstanding permits |
| * to be processed |
| * |
| * @return <code>true</code> if there are no outstanding |
| * permits |
| */ |
| private boolean isEmpty() { |
| return head == null; |
| } |
| |
| /** |
| * Moves a <code>PermitEntry</code> to the back of the list. |
| * An entry is moved to the back upon renewal. If we move the current head |
| * to the back, we notify to allow the expiry time of the next element |
| * to be observed |
| * |
| * @param entry The entry to move |
| */ |
| private void moveToBack(PermitEntry entry) { |
| boolean movedHead = entry == head; |
| if (entry != tail) { // nothing to move / no need to notify if already at back |
| PermitEntry previous = entry.entryBefore; |
| PermitEntry after = entry.entryAfter; |
| |
| tail.entryAfter = entry; |
| entry.entryBefore = tail; |
| entry.entryAfter = null; |
| tail = entry; |
| |
| after.entryBefore = previous; |
| if (!movedHead) { |
| previous.entryAfter = after; |
| } else { |
| head = after; |
| lock.notify(); |
| } |
| } |
| } |
| |
| /** |
| * Removes the head entry (without notifications) |
| */ |
| private void removeHead() { |
| head = head.entryAfter; |
| if (head != null) { |
| head.entryBefore = null; |
| } else { |
| LOG.debug("Permit list empty following removal"); |
| } |
| } |
| |
| /** |
| * Notifies all listeners of the expiry of a specified target object |
| * |
| * @param target The expired target object |
| */ |
| private void notifyExpiry(Object target) { |
| synchronized (listeners) { |
| for (PermitExpirationListener listener : listeners) { |
| listener.permitExpired(target); |
| } |
| } |
| } |
| |
| /** |
| * An entry in a linked list of permits. |
| * |
| * |
| */ |
| private class PermitEntry implements TimedPermit { |
| |
| private PermitEntry entryBefore; |
| |
| private PermitEntry entryAfter; |
| |
| private long expiryTime; |
| |
| private Object o; |
| |
| private boolean isCancelled; |
| |
| PermitEntry(Object o) { |
| this.o = o; |
| extendLifetime(); |
| } |
| |
| /** |
| * Renews this permit. If we are cancelled, no action is taken. |
| * Otherwise, our lifetime is extended, and we move to the back of the |
| * list |
| */ |
| public void renew() { |
| synchronized (lock) { |
| if (!isCancelled) { |
| extendLifetime(); |
| moveToBack(this); |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Entry has been renewed. New expiry time: " |
| + expiryTime); |
| } |
| } |
| } |
| } |
| |
| /** |
| * @return This permits target object |
| */ |
| Object getTarget() { |
| return o; |
| } |
| |
| /** |
| * @return the time remaining, in ms until this permit |
| * expires |
| */ |
| long timeToExpiry() { |
| return expiryTime - System.currentTimeMillis(); |
| } |
| |
| /** |
| * @return <code>true</code> if this permit has been cancelled and should be |
| * removed |
| */ |
| boolean isCancelled() { |
| return isCancelled; |
| } |
| |
| /** |
| * Cancels this permit. |
| * If we are either expired or already cancelled, no action is taken. |
| * Otherwise, we mark ourself as cancelled. |
| * If we are at the head of the permit list, we notify to allow |
| * the notification thread to take any required action. |
| */ |
| public boolean cancel() { |
| synchronized (lock) { |
| if (isCancelled) { |
| LOG.debug("Ignoring cancel request"); |
| return false; |
| } |
| isCancelled = true; |
| LOG.debug("Entry has been successfully cancelled"); |
| if (this == head) { |
| LOG.debug("Head entry cancelled - notifying"); |
| lock.notify(); |
| } |
| return true; |
| } |
| } |
| |
| /** |
| * Marks this permit as cancelled |
| */ |
| void markCancelled() { |
| isCancelled = true; |
| } |
| |
| /** |
| * Extends our lifetime |
| */ |
| private void extendLifetime() { |
| expiryTime = System.currentTimeMillis() + lifetime; |
| } |
| |
| } |
| |
| /** |
| * Services the head of the permit list, blocking until work is available. |
| * Cancelled and expired entries are removed (listeners are notified of all |
| * expired entries) |
| * |
| */ |
| private class ExpiryNotifier implements Runnable { |
| |
| public void run() { |
| try { |
| LOG.debug("ExpiryNotifier starting"); |
| while (processHeadEntry()) { |
| continue; |
| } |
| LOG.debug("ExpiryNotifier closing"); |
| } catch (RuntimeException e) { |
| LOG.error("Unexpected exception on expiry notifier", e); |
| } |
| |
| } |
| |
| /** |
| * Waits for cancellation / expiration of the head entry. |
| * If the head entry is expired, listeners are notified. |
| * |
| * @return <code>true</code> if the entry is processed, |
| * <code>false</code> if we become closed while waiting for a result |
| */ |
| private boolean processHeadEntry() { |
| PermitEntry toExpire = null; |
| try { |
| synchronized (lock) { |
| while (!isClosed && isEmpty()) { |
| lock.wait(); |
| } |
| if (isClosed) { |
| return false; |
| } else { |
| toExpire = processFirst(); |
| } |
| } |
| } catch (InterruptedException e) { |
| throw new RuntimeException(e); |
| } |
| if (toExpire != null) { |
| notifyExpiry(toExpire.getTarget()); |
| } |
| return true; |
| } |
| |
| /** |
| * Examines the head of the list for cancellation or expiry. |
| * We block until either the entry is cancelled, expires, or we are |
| * closed |
| * |
| * @return The previous head entry - if it has expired. |
| * <code>null</code> if the entry was cancelled, or we were closed |
| */ |
| private PermitEntry processFirst() { |
| PermitEntry entry = head; |
| boolean expired = false; |
| long timeToExpiry = entry.timeToExpiry(); |
| while (!isClosed && !entry.isCancelled() && timeToExpiry > 0) { |
| try { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Waiting for head entry to expire: " |
| + timeToExpiry + "ms"); |
| } |
| lock.wait(timeToExpiry); |
| } catch (InterruptedException e) { |
| throw new RuntimeException("Unexpected interrupt"); |
| } |
| timeToExpiry = entry.timeToExpiry(); |
| } |
| |
| if (entry.isCancelled()) { |
| LOG.debug("Head entry is cancelled. Removing"); |
| removeHead(); |
| } else if (!isClosed) { |
| LOG.debug("Head entry has expired"); |
| entry.markCancelled(); |
| removeHead(); |
| expired = true; |
| } |
| return expired ? entry : null; |
| } |
| |
| } |
| |
| } |