Serve-expired and cachedb module

Hi,

I’m using Unbound 1.26.0 on OpenSUSE Tumbleweed and can’t get the serve-expired option to work with the cachedb (Redis) module.

serve-expired works correctly when the expired response is in the Unbound cache (RAM), but it doesn’t work when the RAM cache is flushed via systemctl restart or unbound-control flush_zone.

In this first test, the response TTL matches serve-expired-reply-ttl, showing that serve-expired works as expected when the record is in RAM:

localhost:~ # unbound-control set_option prefetch no
ok
localhost:~ # unbound-control set_option serve-expired-client-timeout 0
ok
localhost:~ # unbound-control set_option serve-expired-reply-ttl 77
ok
localhost:~ # unbound-control flush_zone +c opensuse .pool .ntp .org
ok removed 1 rrsets, 1 messages and 0 key entries
localhost:~ # drill opensuse.pool.ntp.org|awk ‘$1~/.$/||$2==“Query”’
opensuse .pool .ntp .org. 130 IN A 185.19.184.35
opensuse .pool .ntp .org. 130 IN A 204.216.214.76
opensuse .pool .ntp .org. 130 IN A 162.159.200.123
opensuse .pool .ntp .org. 130 IN A 95.110.254.234
;; Query time: 134 msec
localhost:~ # sleep 130
localhost:~ # drill opensuse.pool.ntp.org|awk ‘$1~/.$/||$2==“Query”’
opensuse .pool .ntp .org. 77 IN A 204.216.214.76
opensuse .pool .ntp .org. 77 IN A 162.159.200.123
opensuse .pool .ntp .org. 77 IN A 95.110.254.234
opensuse .pool .ntp .org. 77 IN A 185.19.184.35
;; Query time: 0 msec
localhost:~ # # TTL = serve-expired-reply-ttl (77) and Query time 0 msec = serve expired OK
localhost:~ #

If you flush the Unbound RAM with unbound-control flush_zone, it no longer works. The expired response is in the Redis database but is not served:

localhost:~ # unbound-control set_option prefetch no
ok
localhost:~ # unbound-control set_option serve-expired-client-timeout 0
ok
localhost:~ # unbound-control set_option serve-expired-reply-ttl 77
ok
localhost:~ # unbound-control flush_zone +c opensuse .pool .ntp .org
ok removed 1 rrsets, 1 messages and 0 key entries
localhost:~ # drill opensuse.pool.ntp.org|awk ‘$1~/.$/||$2==“Query”’
opensuse .pool .ntp .org. 130 IN A 93.94.88.50
opensuse .pool .ntp .org. 130 IN A 185.157.229.254
opensuse .pool .ntp .org. 130 IN A 85.199.214.99
opensuse .pool .ntp .org. 130 IN A 162.159.200.1
;; Query time: 116 msec
localhost:~ # unbound-control flush_zone opensuse .pool .ntp .org
ok removed 1 rrsets, 1 messages and 0 key entries
localhost:~ # sleep 130
localhost:~ # drill opensuse.pool.ntp.org|awk ‘$1~/.$/||$2==“Query”’
opensuse .pool .ntp .org. 130 IN A 95.110.254.234
opensuse .pool .ntp .org. 130 IN A 172.232.209.103
opensuse .pool .ntp .org. 130 IN A 85.199.214.99
opensuse .pool .ntp .org. 130 IN A 172.232.208.229
;; Query time: 97 msec
localhost:~ # # TTL = 130 (original ttl) and Query time 97 msec = serve expired KO
localhost:~ #

However, if you flush the Unbound RAM but the TTL has not expired yet, the response from the Redis cache is served correctly:

localhost:~ # unbound-control set_option prefetch no
ok
localhost:~ # unbound-control set_option serve-expired-client-timeout 0
ok
localhost:~ # unbound-control set_option serve-expired-reply-ttl 77
ok
localhost:~ # unbound-control flush_zone +c opensuse .pool .ntp .org
ok removed 1 rrsets, 1 messages and 0 key entries
localhost:~ # drill opensuse.pool.ntp.org|awk ‘$1~/.$/||$2==“Query”’
opensuse .pool .ntp .org. 130 IN A 37.247.53.178
opensuse .pool .ntp .org. 130 IN A 89.46.74.148
opensuse .pool .ntp .org. 130 IN A 172.232.208.229
opensuse .pool .ntp .org. 130 IN A 162.159.200.123
;; Query time: 84 msec
localhost:~ # unbound-control flush_zone opensuse .pool .ntp .org
ok removed 1 rrsets, 1 messages and 0 key entries
localhost:~ # drill opensuse.pool.ntp.org|awk ‘$1~/.$/||$2==“Query”’
opensuse .pool .ntp .org. 116 IN A 37.247.53.178
opensuse .pool .ntp .org. 116 IN A 89.46.74.148
opensuse .pool .ntp .org. 116 IN A 172.232.208.229
opensuse .pool .ntp .org. 116 IN A 162.159.200.123
;; Query time: 0 msec
localhost:~ # # TTL = 116 and Query time 0 msec = cachedb OK only if ttl is not expired
localhost:~ #

In summary: if the expired data is in the Unbound cache (RAM), serve-expired works perfectly. If it is only in the Redis database and not in RAM (e.g., because the service has been restarted), serve-expired fails to serve the stale record and triggers a new upstream query instead, completely ignoring the serve-expired-client-timeout 0 setting.

Is this a known issue, or is there a way to fix this behavior?

Thanks!