Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 60 additions & 3 deletions src/dns.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#include <linux/spinlock.h>
#include <linux/stdarg.h>
#include <linux/string.h>
#include <linux/timer.h>
#include <linux/uio.h>
#include <linux/umh.h>
#include <linux/wait.h>
Expand Down Expand Up @@ -71,8 +72,54 @@ static void dnsfs_cache_remove_entry(struct dnsfs_config *cfg,
call_rcu(&entry->rcu, dnsfs_cache_free_rcu);
}

/* Schedule the next purge sweep while the cache is non-empty. Only the kthread
* runs the sweep, so do nothing without one; timer_pending keeps a burst of
* stores from repeatedly pushing the deadline out.
*/
static void dnsfs_cache_arm_timer(struct dnsfs_config *cfg)
{
if (cfg->query_thread && !timer_pending(&cfg->cache_timer))
mod_timer(&cfg->cache_timer, jiffies + DNSFS_PURGE_INTERVAL_SEC * HZ);
}

/* Timer/softirq context: do only what the context allows -- no cache walk, no
* allocation, no sleeping (I6). Flag the sweep and wake the kthread, which does
* the reaping under query_lock in process context.
*/
static void dnsfs_cache_timer_fn(struct timer_list *t)
{
struct dnsfs_config *cfg = timer_container_of(cfg, t, cache_timer);

atomic_set(&cfg->purge_pending, 1);
wake_up(&cfg->query_wait);
}

/* Time-driven counterpart to the opportunistic sweep in dnsfs_cache_lookup:
* reap every entry whose TTL has elapsed and that is not mid-refresh, so an
* idle mount does not keep dead entries alive until the next miss or memory
* pressure. Runs in the kthread (process context) under query_lock, never in
* the timer itself. An entry being served stale (refreshing) is left for the
* demand path to replace. Re-arm while entries remain so later expiries are
* still collected.
*/
static void dnsfs_cache_purge_expired(struct dnsfs_config *cfg)
{
struct dnsfs_cache_entry *entry;
struct dnsfs_cache_entry *tmp;

mutex_lock(&cfg->query_lock);
list_for_each_entry_safe (entry, tmp, &cfg->cache, list) {
if (time_after_eq(jiffies, entry->expiry) && !entry->refreshing)
dnsfs_cache_remove_entry(cfg, entry);
}
if (!list_empty(&cfg->cache))
dnsfs_cache_arm_timer(cfg);
mutex_unlock(&cfg->query_lock);
}

int dnsfs_cache_init(struct dnsfs_config *cfg)
{
timer_setup(&cfg->cache_timer, dnsfs_cache_timer_fn, 0);
return rhashtable_init(&cfg->cache_ht, &dnsfs_cache_params);
}

Expand All @@ -90,6 +137,11 @@ void dnsfs_free_config(struct dnsfs_config *cfg)
kthread_stop(cfg->query_thread);
cfg->query_thread = NULL;
}
/* The kthread is the only side that re-arms the timer, so once it is
* stopped timer_shutdown_sync cancels any pending or running sweep for
* good, before the cache it walks is drained below.
*/
timer_shutdown_sync(&cfg->cache_timer);
if (cfg->sock)
sock_release(cfg->sock);
while (!list_empty(&cfg->cache)) {
Expand Down Expand Up @@ -942,6 +994,7 @@ static bool dnsfs_cache_store(struct dnsfs_config *cfg,
entry->generation = atomic_long_inc_return(&cfg->cache_generation);
stored = dnsfs_cache_insert_entry(cfg, entry);
dnsfs_cache_evict_tail(cfg);
dnsfs_cache_arm_timer(cfg);
return stored;
}

Expand Down Expand Up @@ -972,6 +1025,7 @@ static bool dnsfs_cache_store_error(struct dnsfs_config *cfg,
entry->generation = atomic_long_inc_return(&cfg->cache_generation);
stored = dnsfs_cache_insert_entry(cfg, entry);
dnsfs_cache_evict_tail(cfg);
dnsfs_cache_arm_timer(cfg);
return stored;
}

Expand Down Expand Up @@ -1093,9 +1147,12 @@ int dnsfs_query_thread(void *data)
for (;;) {
struct dnsfs_query_request *req = NULL;

wait_event_interruptible(
cfg->query_wait,
kthread_should_stop() || !list_empty(&cfg->query_queue));
wait_event_interruptible(cfg->query_wait,
kthread_should_stop() ||
!list_empty(&cfg->query_queue) ||
atomic_read(&cfg->purge_pending));
if (atomic_xchg(&cfg->purge_pending, 0))
dnsfs_cache_purge_expired(cfg);
spin_lock(&cfg->query_queue_lock);
if (!list_empty(&cfg->query_queue)) {
req = list_first_entry(&cfg->query_queue,
Expand Down
8 changes: 8 additions & 0 deletions src/dnsfs.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <linux/rhashtable.h>
#include <linux/shrinker.h>
#include <linux/spinlock.h>
#include <linux/timer.h>
#include <linux/types.h>
#include <linux/wait.h>

Expand All @@ -29,6 +30,11 @@
#define DNSFS_MAX_TTL_SEC 3600
#define DNSFS_RECORD_TEXT_MAX 384
#define DNSFS_MAX_CACHE_ENTRIES 64
/* How often the cache_timer sweeps expired entries (E2). Kept well above the
* gap between an entry's expiry and a demand read, so the M11 stale-serve path
* still wins for entries that are actually read soon after they expire.
*/
#define DNSFS_PURGE_INTERVAL_SEC 5
#define DNSFS_STORAGE_INDEX "index"
#define DNSFS_MAX_STORAGE_CHUNKS 64
#define DNSFS_MAX_STORAGE_SIZE (DNSFS_CHUNK_SIZE * DNSFS_MAX_STORAGE_CHUNKS)
Expand Down Expand Up @@ -81,6 +87,8 @@ struct dnsfs_config {
struct rhashtable cache_ht;
unsigned int cache_entries;
atomic_long_t cache_generation;
struct timer_list cache_timer; /* E2: time-driven expired-entry purge */
atomic_t purge_pending; /* timer asks the kthread to sweep */
/* Query kthread queue: readers enqueue misses/refreshes here and either
* wait or coalesce onto a request already on query_pending (in-flight
* dedup).
Expand Down
40 changes: 40 additions & 0 deletions tests/driver.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dns_bad_index_port=$((22000 + ($$ % 5000)))
dns_publisher_port=$((29000 + ($$ % 5000)))
dns_multi_port=$((21000 + ($$ % 5000)))
dns_signal_port=$((46000 + ($$ % 3000)))
dns_purge_port=$((33000 + ($$ % 3000)))
dns_count=${TMPDIR:-/tmp}/dnsfs-count.$$
parallel_dir=${TMPDIR:-/tmp}/dnsfs-parallel.$$
storage_out=${TMPDIR:-/tmp}/dnsfs-storage.$$
Expand Down Expand Up @@ -725,6 +726,44 @@ PY
stop_dns
}

# E2: the cache_timer purges expired entries time-driven, with no read to trigger
# the demand-path sweep. Fill one entry (TTL 1), never read it again, and after
# the purge interval elapses the entry must be gone -- a read then blocks on the
# slow resolver (a real miss), where a lingering stale entry would have answered
# instantly. THOROUGH only (sleeps past TTL + purge interval).
test_purge_timer()
{
[ "$thorough" = 1 ] || return 0
start_dns "$dns_purge_port" --ttl1 --delay-ms=600 --count-file "$dns_count"
rm -f "$dns_count"
mount_dnsfs -o nameserver=127.0.0.1,port="$dns_purge_port",timeout=2000,retries=1 example.org
expect_file_content "$mnt/TXT" "dnsfs live" "purge timer fill"
expect_dns_count 1 "purge timer fill count"
sleep 7
python3 - "$mnt/TXT" "$dns_count" <<'PY'
import pathlib
import sys
import time

path = pathlib.Path(sys.argv[1])
count_file = pathlib.Path(sys.argv[2])
start = time.monotonic()
got = path.read_text().strip()
elapsed = time.monotonic() - start
if got != "dnsfs live":
raise SystemExit(f"unexpected purge-timer read: {got!r}")
# A lingering stale entry answers instantly; a real miss blocks on the 600ms
# resolver, proving the timer purged the expired entry with no read to trigger it.
if elapsed < 0.3:
raise SystemExit(f"read too fast ({elapsed:.3f}s): entry was not purged")
count = int(count_file.read_text().strip())
if count != 2:
raise SystemExit(f"expected 2 wire queries after purge + miss, got {count}")
PY
unmount_dnsfs
stop_dns
}

# A signal must abort a read blocked on a slow resolver promptly, not wait out
# the whole query (E1). The resolver delays every answer 3s; a cat is
# interrupted ~0.3s in and must die from the signal well before that, proving
Expand Down Expand Up @@ -1464,6 +1503,7 @@ test_cache_eviction
test_cache_reclaim
test_positive_ttl
test_async_ttl_refresh
test_purge_timer
test_signal_interrupt
test_tcp_fallback
test_multi_record
Expand Down