/* * Copyright © 2013. Palomino Labs (http://palominolabs.com) * * 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.palominolabs.crm.sf.soap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; import java.util.concurrent.Semaphore; /** * A mechanism for limiting the number of concurrent calls for a given org. Salesforce only allows a limited number of * concurrent calls to be made. When that limit is breached, an UnexpectedErrorFault_Exception is thrown, which is too * vague to be useful as a means of catching that specific exception. So, to reduce the possibility of hitting the limit * (it can never be eliminated since other tools could be accessing that salesforce org), each PartnerConnection gets a * token every time it needs to make a call and releases it after the call. * * To allow for runtime tuning of the number of permits, a new object is initialized with 0 permits. The proper permit * limit must be set by calling setMaxPermits. setMaxPermits can subsequently be called safely at any point to adjust * the max number of permits allowed. * * New instances should be configured with setMaxPermits(). */ @SuppressWarnings("AccessToStaticFieldLockedOnInstance") @ThreadSafe final class CallSemaphore { private static final Logger logger = LoggerFactory.getLogger(CallSemaphore.class); /** * semaphore starts at 0 capacity; must be set by setMaxPermits before use */ private final ResizeableSemaphore semaphore = new ResizeableSemaphore(); /** * how many concurrent calls are allowed as governed by this semaphore */ @GuardedBy("this") private int maxPermits = 0; /* * Must be synchronized because the underlying int is not thread safe */ /** * Set the max number of tokens. Must be greater than zero. * * Note that if there are more than the new max number of permits currently outstanding, any currently blocking * threads or any new threads that start to block after the call will wait until enough permits have been released * to have the number of outstanding permits fall below the new maximum. In other words, it does what you probably * think it should. * * @param newMax the new max number of permits */ synchronized void setMaxPermits(int newMax) { if (newMax < 1) { throw new IllegalArgumentException("Semaphore size must be at least 1, was " + newMax); } int delta = newMax - this.maxPermits; if (delta == 0) { return; } else if (delta > 0) { // new max is higher, so release that many permits logger.debug("Increasing size by " + delta + " to " + newMax); this.semaphore.release(delta); } else { delta *= -1; // delta < 0. // reducePermits needs a positive #, though. logger.debug("Decreasing size by " + delta + " to " + newMax); this.semaphore.reducePermits(delta); } this.maxPermits = newMax; } /** * Release a permit back to the semaphore. Make sure not to double-release. */ void release() { this.semaphore.release(); } /** * Get a permit, blocking if necessary. * * @throws InterruptedException if interrupted while waiting for a token */ void acquire() throws InterruptedException { this.semaphore.acquire(); } /** * A trivial subclass of Semaphore that exposes the reducePermits call to the parent class. Doug Lea says it's ok... * http://osdir.com/ml/java.jsr.166-concurrency/2003-10/msg00042.html */ private static final class ResizeableSemaphore extends Semaphore { private static final long serialVersionUID = 1L; /** * Create a new semaphore with 0 permits. */ ResizeableSemaphore() { super(0); } /* * expose the method to the parent class */ @SuppressWarnings("EmptyMethod") @Override protected void reducePermits(int reduction) { super.reducePermits(reduction); } } }