I am looking for a semaphore-like construct with the ability to "force" an acquisition, even when maximum capacity is reached, essentially overflowing the semaphore.
I am imagining an interface similar to this:
init(int n) // initialize with n permits
boolean acquire() // try to acquire a permit, returning true if succeeded (non-blocking)
void forceAcquire() // acquire a permit even when none are free, potentially making the number of available permits negative
void release() // increase the amount of available permits
Additional requirements:
acquire
is non blocking (returns immediately even on failureI am using Spring Boot 3 with Java 17.
I have looked at Redisson's RSemaphore, which doesn't support "overflow".
I think it may be possible to achieve the same result with manual tracking of overflow using RAtomicLong:
class OverflowableSemaphore {
RSemaphore semaphore;
RAtomicLong overflowCounter;
public OverflowableSemaphore(int n) {
this.semaphore.trySetPermits(n);
}
public boolean acquire() {
return semaphore.tryAcquire();
}
public void forceAcquire() {
boolean acquired = acquire();
if (acquired) {
return;
}
else {
overflowCounter.incrementAndGet();
}
}
public void release() {
while (true) {
long currentOverflow = overflowCounter.get();
if (currentOverflow > 0) {
if (overflowCounter.compareAndSet(currentOverflow, currentOverflow - 1)) {
return;
}
// else: retry due to race
}
else {
semaphore.release();
return;
}
}
}
}
However, this seems quite inefficient as it potentially requires many back and forth calls to Redis.
Additionally, I would like to avoid custom implementations if at all possible, and prefer something more well tested.
Is there an existing solution to this problem?
And if not, how can I improve my proposed solution?
this seems quite inefficient as it potentially requires many back and forth calls to Redis.
That's probably going to be a property of any Redis-based approach to your particular problem. I expect that you already have it in RSemaphore
alone, though of course you have it worse if you add more stuff on top.
Is there an existing solution to this problem?
It sounds like you're asking for a library recommendation. That's off-topic here. For the record, however, what you ask for is pretty unusual, even without making it distributed as you want to do, so I would be surprised if you could find it already built.
how can I improve my proposed solution?
It is awkward to implement your OverflowableSemaphore
in terms of an ordinary (sort of) counting semaphore. I would recommend building yours on top of more primitive objects. In particular,
It can only cause you grief to have an overflow count separate from the main count, so I would go with a single, combined counter. Presumably that would be modeled on the Java side as an RAtomicLong
.
You will need a way to avoid races in initializing semaphore instances, and to support that you will need at least one other shared item. This could reasonably be another RAtomicLong
used as a state indicator -- uninitialized, initializing, initialized.
You can and should get good mileage out of RAtomicLong
by employing the usual techniques for lock-free atomic programming, but that's probably not enough. And yes, that will sometimes contribute to more round trips to Redis and back.
Unless you want to rely exclusively on spin locking, you will need a way to block on changes to the Redis-stored value of an object. You might want this to avoid spinning during a contested initialization of the semaphore. You will probably want this to make threads block on non-forcing semaphore decrements when appropriate. Redisson's IncrByListener should be helpful here, combined with local Object.wait()
/ Object.notify()
. This is likely the most difficult part to do right.