Now here’s a little trick for those of you hacking around with third-party tools, trying to extend them without fully understanding them (yet!). Assume the following situation:
- You want to extend a library that exposes a hierarchical data model (let’s assume you want to extend Apache Jackrabbit)
- That library internally checks access rights before accessing any nodes of the content repository
- You want to implement your own access control algorithm
- Your access control algorithm will access other nodes of the content repository
- … which in turn will again trigger access control
- … which in turn will again access other nodes of the content repository
… Infinite recursion, possibly resulting in a
StackOverflowError, if you’re not recursing breadth-first.
Now, you have two options:
- Take the time, sit down, understand the internals, and do it right. You probably shouldn’t recurse into your access control once you’ve reached your own extension. In the case of extending Jackrabbit, this would be done by using a System Session to further access nodes within your access control algorithm. A System Session usually bypasses access control.
- Be impatient, wanting to get results quickly, and prevent recursion with a trick
Of course, you
really should opt for option 1. But who has the time to understand everything? ;-)
Here’s how to implement that trick.
/**
* This thread local indicates whether you've
* already started recursing with level 1
*/
static final ThreadLocal<Boolean> RECURSION_CONTROL
= new ThreadLocal<Boolean>();
/**
* This method executes a delegate in a "protected"
* mode, preventing recursion. If a inadvertent
* recursion occurred, return a default instead
*/
public static <T> T protect(
T resultOnRecursion,
Protectable<T> delegate)
throws Exception {
// Not recursing yet, allow a single level of
// recursion and execute the delegate once
if (RECURSION_CONTROL.get() == null) {
try {
RECURSION_CONTROL.set(true);
return delegate.call();
}
finally {
RECURSION_CONTROL.remove();
}
}
// Abort recursion and return early
else {
return resultOnRecursion;
}
}
/**
* An API to wrap your code with
*/
public interface Protectable<T> {
T call() throws Exception;
}
This works easily as can be seen in this usage example:
public static void main(String[] args)
throws Exception {
protect(null, new Protectable<Void>() {
@Override
public Void call() throws Exception {
// Recurse infinitely
System.out.println("Recursing?");
main(null);
System.out.println("No!");
return null;
}
});
}
The recursive call to the
main()
method will be aborted by the protect method, and return early, instead of executing
call()
.
This idea can also be further elaborated by using a
Map
of
ThreadLocals
instead, allowing for specifying various keys or contexts for which to prevent recursion. Then, you could also put an
Integer
into the
ThreadLocal
, incrementing it on recursion, allowing for at most N levels of recursion.
static final ThreadLocal<Integer> RECURSION_CONTROL
= new ThreadLocal<Integer>();
public static <T> T protect(
T resultOnRecursion,
Protectable<T> delegate)
throws Exception {
Integer level = RECURSION_CONTROL.get();
level = (level == null) ? 0 : level;
if (level < 5) {
try {
RECURSION_CONTROL.set(level + 1);
return delegate.call();
}
finally {
if (level > 0)
RECURSION_CONTROL.set(level - 1);
else
RECURSION_CONTROL.remove();
}
}
else {
return resultOnRecursion;
}
}
But again. Maybe you should just take a couple of minutes more and learn about how the internals of your host library really work, and get things right from the beginning… As always, when applying tricks and hacks! :-)
Like this:
Like Loading...
Published by lukaseder
I made jOOQ
View all posts by lukaseder
Very interesting. I think another problem that is typically seen when working with thread local variables is the one caused by the thread being reused by a thread pool. Since the thread pool reuses the threads, you could see unexpected behavior if when the thread is reused, you end up using a thread local variable that is in a inconsistent state. So, the programmer must, somehow, ensure that the thread local variable is in a reusable state by the time the thread in question finishes with it. I think in the example you propose, ideally, the level of recursion should always end up at 0, but if that wasn’t the case, that could cause unexpected behavior if the thread is used, and then reuses a thread local variable whose state is inconsistent (i.e. initially starting with a recursion level of 10). Could that also happen in this scenario?
I think this couldn’t happen, unless the thread is somehow returned to the pool while recursing, which is very unlikely. Given the fact that the finally clause will always remove the thread local’s content after finishing the first recursion depth (depth zero), I think there shouldn’t be any problems here…
Interesting (like all your articles)!
I have used this trick too. Very useful, especially when you give some freedom of configuration to your end-user :)
It could also be implemented with auto-closeable:
That’s an intriguing idea, using
AutoCloseable
for this! Would you mind posting a larger code example?Here is a sample code.
I realize now that it cannot cancel the recursion “softly” with a default value like your approach. But it’s still better than a brutal stackoverflow :)
Unfortunately, this is not allowed:
I also use this pattern to wrap java.util.concurrent.locks.Lock (the close() method calls unlock()).
Nice, I can see how this works out. Thanks for the contribution!
(I hope that my edits to fix the markup were correct)
Thanks for fixing the markup!
There is a similar discussion here (“active” vs “passive”) :
https://code.google.com/p/guava-libraries/issues/detail?id=683#c15