0

I have a Caffeine Cache of String id and LocalDateTime (when record was inserted)

I am trying to follow the principle of implementing expiry interface in order to manage expiry times

A code snippet i took from stackoverflow is below

LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .expireAfter(new Expiry<Key, Graph>() {
      public long expireAfterCreate(Key key, Graph graph, long currentTime) {
        return 1000;
      }
      public long expireAfterUpdate(Key key, Graph graph, 
          long currentTime, long currentDuration) {
        return 1000;
      }
      public long expireAfterRead(Key key, Graph graph,
          long currentTime, long currentDuration) {
        return currentDuration;
      }
    })
    .build(key -> createExpensiveGraph(key));

What I am struggling to understand is; lets say I put a key in the map, of "A" and a LocalDateTime.now() - it will expire after one second, but assuming i keep repopulating the map with LocalDateTime.now() for key A then I would like the timer to be reset

So in theory, if I infinitely called cache.put("A", LocalDateTime.now()) then I would never get an expiry notification

The actual problem I am trying to solve is, we get an initial update for some keys, and every n number of seconds they should update - hence re adding them to my cache. I don't want the initial insert to trigger the expiry, only the most recent insert for a given key if it exceeds the imposed time constraint

I hope the above makes sense - it is quite difficult to explain

Sample test code -

final Cache<String, LocalDateTime> cache = Caffeine.newBuilder()
        .executor(Runnable::run)
        .ticker(ticker::read)
        .removalListener((key, value, cause) -> {
            if (cause.wasEvicted()) {
                System.out.printf("key=%s, value=%s", key, value);
            }
        })
        .expireAfterWrite(1, TimeUnit.SECONDS)
        .weakKeys()
        .weakValues()
        .build();

Example 2

public class Main implements Runnable{

    final Cache<String, LocalDateTime> cache = Caffeine.newBuilder()
            .executor(Runnable::run)
            .scheduler(createScheduler())
            .removalListener((key, value, cause) -> {
                if (cause == RemovalCause.EXPIRED) {
                    System.out.printf("key=%s, value=%s", key, value);
                }
            })
            .weakKeys()
            .weakValues()
            .expireAfterWrite(10, TimeUnit.SECONDS)
            .build();

    private Scheduler createScheduler() {
        return forScheduledExecutorService(newSingleThreadScheduledExecutor());
    }


    LocalDateTime create(String k) {
        return LocalDateTime.now();
    }

    public void add(String a) {
        cache.put(a, LocalDateTime.now());
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new Main());
        thread.start();
    }

    @Override
    public void run() {
        add("a");
        add("b");
        for(int i = 0; i < 100; i++) {
            System.out.println("adding a" + i);
            add("a");
            try {
                Thread.sleep(900);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
Biscuit128
  • 5,218
  • 22
  • 89
  • 149
  • The expiry maintains a single timestamp per entry, so each of those calls return what the next value should be. Your code example says that the insert or update set the timestamp to now + 1 second, and on a read do not change that value. That sounds like what you want, also equivalent to `expireAfterWrite(1 sec)`. Sometimes people get confused thinking it is maintaining 3 timestamps and not a single one, if that was your confusion? You can use `ticker` to fake time and experiment to see the behavior, if that helps. – Ben Manes May 26 '23 at 14:02
  • Hi @BenManes Thank you very much for your reply - I have worked through the example on your documentation, relating to the Ticker (FakeTicker) and can see using `.expireAfterWrite(1, TimeUnit.SECONDS)` works as expected - but it never seems to give me a call back to `removalListener((key, value, cause) ` - is this expexted behaviour? The call back is essential to what I am trying to achieve. I have added my sample test code to the original post - Thanks – Biscuit128 May 26 '23 at 17:43
  • You have to specify a `scheduler` if you want prompt notification. The cache does not create its own threads an a plain `Executor` lacks scheduling capabilities. `Scheduler.systemScheduler()` will use a shared JVM-wide thread introduced in Java 9. If not set, then the cache will piggyback on other reads or writes to perform maintenance work to eventually remove the expired entry. – Ben Manes May 27 '23 at 01:12
  • Thank you very much for replying @BenManes i really appreciate it - I have pasted an example under Example 2 to demonstrate my next lack of understanding :) Assuming that "a" get's populated consistently but b does not then ideally b should be evicted for expiry but the call back does not get triggered (when thread.sleep < 1 second) if i set a break point in the removal listener b is then removed - i assume it is because there is a chance of the worker thread to catchup? What tweaks do i need to enable in order to ensure that writes do not effect removal detection please? Anything >=1 works – Biscuit128 May 27 '23 at 08:06
  • Just to further the above - I have tried `forScheduledExecutorService(newScheduledThreadPool(Runtime.getRuntime().availableProcessors()));` but this did not seem to help - i had assumed the single thread approach was too busy with the `RemovalCause.REPLACED` events? – Biscuit128 May 27 '23 at 08:08
  • Your test exposes a flaw in an [optimization](https://github.com/ben-manes/caffeine/commit/2c5c1d96106cebb8d036043ffdd897f053fd32bb) that tries to avoid overwhelming the cache on writes. There is a 1 second tolerance to update the timestamp directly and not reorder the entry on the expiration queue. At 0.9s it always falls within this threshold and "a" stays ordered before "b", causing expirations to stop searching. This applies to `expireAfterWrite` which reorders on a doubly-linked list so it only has to scan the first few elements. If absent it caused writes performance to degrade. – Ben Manes May 27 '23 at 16:37
  • That flaw is not present if you use `expireAfter(expiry)` which came later and uses a more advanced data structure, a timing wheel. That has the same optimization but is not a naive ordering, yet is still O(1) so very fast. Using that configuration and your tests works perfectly. I'll have to explore improvements here. Had the timing wheel been implemented first then `expireAfterWrite` would simply be a convenience configuration. It wasn't, so I did not refactor working code away. Either it could switch or the optimization could be probabilistic to not get pinned in a bad state. – Ben Manes May 27 '23 at 16:41
  • @BenManes very interesting - thank you for the information - unfortunately i am constrained to java 8 due to company so am stuck with `expireAfterWrite` it seems. My actual problem definition is to pull quotes from market if no tick within a period - these tick at a ms interval. – Biscuit128 May 28 '23 at 17:28
  • On the first invocation of `removalListener` i need to grab all of the keys - but it appears as if they all get GC'ed before i have chance to grab them - do you know if this is possible at all? I have tried cloning with the copy constructor as well as `cache.asMap().keySet().stream().collect(Collectors.toSet())` but before i get a chance to do anything they are already gone - do you have any suggestions for this at all? This is probably my last question - sorry for so many! – Biscuit128 May 28 '23 at 17:29
  • `expireAfter(expiry)` was added in v2.5.0, which was Java 8 based, so you could still use that like your original example to workaround this bug. I'll fix it going forward also. Thanks for being patient as I know it can get frustrating to figure out a library and its quirks. – Ben Manes May 28 '23 at 17:39
  • Do you really need weak keys/values? The GC can be aggressive and you have an expiration policy, so it seems unnecessary. Weak keys means that if there is no strong reference to the key then it can be discard, and weak values is likewise for the value. That is helpful for life cycle logic of closeable resources, but most caches are for data reuse where the reference will be used only during a user request so held shortly. You probably don't need either weak variant enabled. – Ben Manes May 28 '23 at 17:42
  • @BenManes i think the issue is that i am trying to get the keys from the cache using `cache.asMap().keySet(()` from within the `evictionListener` at which point it is too late? Ultimately trying to say, given the first indication of an expiry - grab everything and perform some action then clear the cache contents manually via `invalidateAll()` or some such – Biscuit128 May 29 '23 at 10:13
  • Oh, the cache does suppress entries that are pending eviction as they shouldn't be usable and may reside until automatically removed. `Cache.policy()` has some methods to review or you could coalesce evictions into a batch for that action. Otherwise it sounds like you need a custom cache where you control the eviction process. Maybe you just need a map and a scheduled executor with a periodic task? – Ben Manes May 29 '23 at 17:14

0 Answers0