EternalCore icon indicating copy to clipboard operation
EternalCore copied to clipboard

GH-1153 Rework delay system with per-entry Instant TTL

Open imDMK opened this issue 2 months ago • 2 comments

Description

This PR refactors the delay handling to eliminate cache-level TTL conflicts.

  • Introduced GuavaDelay with shared logic on top of Guava Cache
  • Removed expireAfterWrite to ensure TTL is controlled per entry via Instant
  • Added separate interfaces: DefaultDelay (with predefined duration) and ExplicitDelay (explicit duration required)
  • Implemented GuavaDefaultDelay and GuavaExplicitDelay
  • Added Delay factory for convenient instance creation
  • Clarified contract and documentation

Result: cleaner API, safer usage, no premature cache evictions. Fixes #1153

Type of change

  • [X] Bug fix (non-breaking change which fixes an issue)
  • [X] Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • [X] This change requires a documentation update

How Has This Been Tested?

  • [X] Localhost server

imDMK avatar Sep 30 '25 09:09 imDMK

/gemini review

imDMK avatar Sep 30 '25 12:09 imDMK

// kontrakt
public interface Delay<TYPE> {

    void mark(TYPE key, Function<TYPE, Duration> durationProviderOverride);

    void unmark(TYPE key);

    boolean isActive(TYPE key);

    Duration getRemaining(TYPE key);

    Instant getExpireAt(TYPE key);

    void extend(TYPE key, Duration extra);

    static <TYPE> Delay<TYPE> ofDefault(Duration defaultDuration, long maximumSize) {
        return new KeyedDelay<>(key -> defaultDuration, maximumSize);
    }

    static <TYPE> Delay<TYPE> ofDynamic(Function<TYPE, Duration> durationProvider, long maximumSize) {
        return new KeyedDelay<>(durationProvider, maximumSize);
    }
}

final class KeyedDelay<TYPE> implements Delay<TYPE> {

    private final Function<TYPE, Duration> durationProvider;
    private final Cache<TYPE, Instant> cache;

    KeyedDelay(Function<TYPE, Duration> durationProvider, long maximumSize) {
        this.durationProvider = durationProvider;
        this.cache = Caffeine.newBuilder()
                .maximumSize(maximumSize)
                .build();
    }

    @Override
    public void mark(TYPE key, Function<TYPE, Duration> durationProviderOverride) {
        Function<TYPE, Duration> functor = durationProviderOverride != null ? durationProviderOverride : durationProvider;
        Duration duration = functor.apply(key);
        
        cache.put(key, Instant.now().plus(duration));
    }

    @Override
    public void unmark(TYPE key) {
        cache.invalidate(key);
    }

    @Override
    public boolean isActive(TYPE key) {
        Instant expire = cache.getIfPresent(key);
        return expire != null && Instant.now().isBefore(expire);
    }

    @Override
    public Duration getRemaining(TYPE key) {
        Instant expire = cache.getIfPresent(key);
        if (expire == null) { 
            return Duration.ZERO;
        }
        
        Duration remaining = Duration.between(Instant.now(), expire);
        return remaining.isNegative() ? Duration.ZERO : remaining;
    }

    @Override
    public Instant getExpireAt(TYPE key) {
        return cache.getIfPresent(key);
    }

    @Override
    public void extend(TYPE key, Duration extra) {
        Instant expire = cache.getIfPresent(key);
        Instant newExpire = (expire != null ? expire : Instant.now()).plus(extra);
        
        cache.put(key, newExpire);
    }
}

uzycie:


final class DelayExample {

    void main(String[] args) throws InterruptedException {
        // Tworzymy Delay z domyślnym czasem np. 5 sekund
        Delay<String> delay = Delay.ofDefault(Duration.ofSeconds(5), 100);

        String key = "hujsonpool";

        // Sprawdzamy, czy mamy delay
        System.out.println("Czy jest delay? " + delay.has(key)); // :: false

        // Oznaczamy delay
        delay.mark(key, null);
        System.out.println("Ustawiono delay dla " + key);

        // Pozostały czas
        System.out.println("Pozostały czas: " + delay.getRemaining(key).toSeconds() + " sekund");

        // Przykry przykład Czekamy 3 sekundy
        Thread.sleep(3000);

        System.out.println("Czy jest delay po 3 sekundach? " + delay.has(key));
        System.out.println("Pozostały czas: " + delay.getRemaining(key).toSeconds() + " sekund");

        // Przedłużamy o 2 sekundy
        delay.extend(key, Duration.ofSeconds(2));
        System.out.println("Przedłużono delay. Pozostały czas: " + delay.getRemaining(key).toSeconds() + " sekund");

        // Czekamy kolejne 5 sekund
        Thread.sleep(5000);
        System.out.println("Czy jest delay po 8 sekundach? " + delay.has(key)); // :: false
        System.out.println("Pozostały czas: " + delay.getRemaining(key).toSeconds() + " sekund");

        // Usuwamy delay
        delay.unmark(key);
        System.out.println("Delay usunięty" + delay.has(key));
    }
}

noyzys avatar Oct 08 '25 17:10 noyzys