AccessControlException while using a custom caching library in x-pack plugin

Hi,

I was trying to implement some custom caching mechanism using the Caffeine library, I got past issues deploying by granting permissions

permission java.lang.RuntimePermission "sun.misc.Unsafe";
permission java.lang.RuntimePermission "accessClassInPackage.sun.misc";
permission java.lang.RuntimePermission "accessDeclaredMembers";
permission org.elasticsearch.secure_sm.ThreadPermission "modifyArbitraryThread";
permission org.elasticsearch.secure_sm.ThreadPermission "modifyArbitraryThreadGroup";

I used the elasticsearch cache class earlier and it worked well but I needed a different caching mechanism not supported, which is why I chose to use Caffeine. I do not know precisely what is going on but it looks like it's trying to create some kind of thread pool.
Is this an issue related to granting permissions? I've wrapped the code in doPriviliged blocks too. Is it a good idea to use libraries like this which interact with core elastic threads? Something to do with modifying Threads may cause performance issues later on. Here is the exception while trying to add to the cache

Aug 28, 2018 5:11:57 PM com.github.benmanes.caffeine.cache.BoundedLocalCache scheduleDrainBuffers
WARNING: Exception thrown when submitting maintenance task
java.lang.Error: java.security.AccessControlException: access denied ("org.elasticsearch.secure_sm.ThreadPermission" "mo
difyArbitraryThreadGroup")
        at java.util.concurrent.ForkJoinWorkerThread$InnocuousForkJoinWorkerThread.createThreadGroup(ForkJoinWorkerThrea
d.java:269)
        at java.util.concurrent.ForkJoinWorkerThread$InnocuousForkJoinWorkerThread.<clinit>(ForkJoinWorkerThread.java:21
6)
        at java.util.concurrent.ForkJoinPool$InnocuousForkJoinWorkerThreadFactory$1.run(ForkJoinPool.java:3471)
        at java.util.concurrent.ForkJoinPool$InnocuousForkJoinWorkerThreadFactory$1.run(ForkJoinPool.java:3469)

It's not ideal, but it's am unfortunate fact of life with external libraries. Elasticsearch ships with some similar permissions for libraries that insist on managing their own threads and timers.

Can you elaborate on why the Elasticsearch cache didn't meet your needs? Perhaps there's a solution there that you aren't aware of.

I did not quite gather the answer from the above. Is it a permission related thing which is not configured correctly and can be fixed or elasticsearch does not allow this, can you elaborate?
I am trying to implement a solution where each entry(token)in my cache will have an expiry time based on expiry_time parameter retrieved from an external call. For example, token1 can expire 2 minutes from now, token2 can expire 3 minutes from now etc.
The elasticsearch CacheBuilder has expireAfterWrite setting which applies to all entries in cache.

@TimV is this something that elastic does not allow?

Caffeine will delegate work to ForkJoinPool.commonPool() by default. It won’t create its own threads, though, and you can disable this behavior. Try using Caffeine.executor(Runnable::run) and the work will be amortized on calling threads instead.

Thanks @ben.manes, I got past the exception, but now it looks like it's adding to the cache but I do not find the key in the cache when I fetch the key. This is how I have initialized the cache, I assume this happens only once? or everytime you do a .put into the cache object?

private static final Cache<String,MyToken> tokenCache = AccessController.doPrivileged((PrivilegedAction<Cache<String,MyToken>>) () -> {
		final com.github.benmanes.caffeine.cache.Cache<String, MyToken> cache = Caffeine.newBuilder().expireAfter(new Expiry<String, MyToken>() {
			@Override
			public long expireAfterCreate(
					String key, MyToken value, long currentTime) {
				// This is for testing
				return 300000;
			}
			@Override
			public long expireAfterUpdate(
					String key, MyToken value, long currentTime, long currentDuration) {
				return currentDuration;
			}
			@Override
			public long expireAfterRead(
					String key, MyToken value, long currentTime, long currentDuration) {
				return currentDuration;
			}
		}).executor(Runnable::run).build();
		return cache;
	});

Usually you would use a LoadingCache to fetch on a miss. From what I see, the expiration time in nanoseconds is quite low (300 us). You can use TimeUnit.MINUTES.toNanos(xyz) or similar to assist in the conversion. This API is a little low-level due to lack of value types, so Duration would have been nicer but result in allocations on hot paths.

2 Likes

That was my bad, for some reason, I'd assumed it's in ms, I switched it to nanos and seems to be working.
I plan on using the normal cache and do a .put as I have some other logic and not as simple as adding into the cache on misses like in the LoadingCache. The method to fetch and load into cache is an overridden method from a framework which is expecting the return and calls another method once it's received.
Lastly, if I plan on using this method of cache eviction for each entry with .executor(Runnable::run).build(), from what you had mentioned above, the calling thread which invoked the method takes care of the eviction depending on the time for each key based on the expireAfter implemenation in my example from above as opposed to adding it to the existing ForkJoinPool of the application?
How does the Custom policy eviction for each entry individually based on Expiry implementation work in Caffeine? Does it schedule some threads to run at the expire time for each entry and if the size of the cache increases, we have more threads scheduled? I am asking this as I want to understand if there is going to be a performance overhead using this implementation v/s a normal Expire After Write implementation which I assume it's more time based where something kicks off and manages the evictions

Great, I'm glad you got it working.

None of the expiration methods create scheduling threads to promptly evict, but rather the cache performs maintenance work when enough read have accrued or writes occured. This avoids the thread issue and is cheap, but doesn't give a guarantee for when removal notifications are published. Maybe someday we'll optionally take advantage of JDK9's system-wide ScheduledExecutor to allow this, but for now either activity or periodically calling cleanUp() are required. Since you aren't using a removal listener, it shouldn't matter.

The fixed policies are implemented by peeking at the head of an LRU list for the next expired entry and reordering an entry on access / write to the tail. That means a nice, cheap O(1) policy.

For variable expiration, expireAfter, instead of using a linked list for strict O(1) we use a hierarchical timer wheel for amortized O(1). That's a priority queue, usually O(lg n), that leverages time to be O(1) by using hashing tricks. You shouldn't see a performance penalty, as the operations are cheap and it scales efficiently.

Since we use only O(1) algorithms throughout, the penalty for various policies are quite small. So far I haven't heard of any performance concerns for either of the expiration algorithms.

1 Like

This topic was automatically closed 28 days after the last reply. New replies are no longer allowed.