Directory Cache. More...
#include <assert.h>#include <errno.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <time.h>#include <bstrlib.h>#include <atalk/cnid.h>#include <atalk/directory.h>#include <atalk/globals.h>#include <atalk/logger.h>#include <atalk/netatalk_conf.h>#include <atalk/queue.h>#include <atalk/server_ipc.h>#include <atalk/util.h>#include <atalk/volume.h>#include "ad_cache.h"#include "dircache.h"#include "directory.h"#include "hash.h"#include "idle_worker.h"Data Structures | |
| struct | deferred_cleanup |
| struct | dircache_stat |
Macros | |
| #define | MAX_DEFERRED_CLEANUPS 5120 |
| #define | get16bits(d) |
| #define | MAX_HINTS_PER_CYCLE 32 |
| #define | HINT_READ_BUF_SIZE |
Enumerations | |
| enum | arc_list_t { ARC_NONE = 0 , ARC_T1 , ARC_T2 , ARC_B1 , ARC_B2 } |
Functions | |
| static hnode_t * | hash_chain_head (hash_t *hash, hashcount_t chain) |
| static hash_val_t | hash_vid_did (const void *key) |
| static int | hash_comp_vid_did (const void *key1, const void *key2) |
| static int | arc_init (size_t cache_size) |
| Initialize ARC cache structures. | |
| static void | arc_destroy (void) |
| Destroy ARC cache structures. | |
| static void | arc_verify_invariants (void) |
| Verify ARC invariants (debug mode). | |
| static void | arc_ensure_ghost_capacity (arc_list_t target_list) |
| Ensure ghost directory has room for one more entry. | |
| static struct dir * | arc_find_victim (q_t *queue, size_t queue_size) |
| Find an evictable victim in a cache queue, skipping curdir. | |
| static void | arc_evict_to_ghost (struct dir *victim, q_t *src, q_t *dst, int dst_list, bool fallback) |
| Evict a victim from a cache queue to the corresponding ghost list. | |
| static int | arc_replace (int in_b2) |
| ARC REPLACE subroutine (from paper Figure 1). | |
| static void | arc_case_i (struct dir *dir) |
| ARC Case I: Cache hit in T1 or T2. | |
| static void | arc_case_ii_adapt_and_replace (struct dir *ghost) |
| ARC Case II: Ghost hit in B1. | |
| static void | arc_case_iii_adapt_and_replace (struct dir *ghost) |
| ARC Case III: Ghost hit in B2. | |
| static void | arc_case_iv (struct dir *dir) |
| ARC Case IV: Complete miss (not in any list). | |
| static hash_val_t | hash_didname (const void *p) |
| static int | hash_comp_didname (const void *k1, const void *k2) |
| static int | should_validate_cache_entry (void) |
| Determine if cache entry should be validated against filesystem. | |
| static void | dircache_evict (void) |
| Remove a fixed number of (oldest) entries from the cache and indexes. | |
| struct dir * | dircache_search_by_did (const struct vol *vol, cnid_t cnid) |
| Search the dircache via a CNID for a directory. | |
| struct dir * | dircache_search_by_name (const struct vol *vol, const struct dir *dir, char *name, size_t len) |
| Search the cache via did/name hashtable. | |
| int | dircache_add (const struct vol *vol, struct dir *dir) |
| create struct dir from struct path | |
| void | dircache_remove (const struct vol *vol, struct dir *dir, int flags) |
| Remove an entry from the dircache. | |
| int | dircache_remove_children (const struct vol *vol, struct dir *dir) |
| Remove all child entries of a directory from the dircache. | |
| int | dircache_reindex_didname (const struct vol *vol, struct dir *dir) |
| Re-insert entry into DID/name index after key change. | |
| void | dircache_promote (struct dir *dir) |
| Promote a cache entry to signal recency. | |
| int | dircache_init (int reqsize) |
| Initialize the dircache and indexes. | |
| void | log_dircache_stat (void) |
| Log dircache statistics. | |
| void | dircache_rfork_shutdown (void) |
| Shutdown the rfork cache — free remaining RFork LRU nodes and sentinel. | |
| void | dircache_dump (void) |
| Dump dircache to /tmp/dircache.PID. | |
| int | dircache_set_validation_params (unsigned int freq) |
| Set directory cache validation frequency. | |
| void | dircache_reset_validation_counter (void) |
| Reset validation counter for consistent testing. | |
| void | process_cache_hints (AFPObj *obj) |
| Process cross-process dircache invalidation hints. | |
| void | dircache_report_invalid_entry (struct dir *dir) |
| Report that a cache entry was invalid when actually used. | |
| void | dircache_remove_children_defer (const struct vol *vol, struct dir *dir) |
| Enqueue a deferred dircache_remove_children operation. | |
| void | dircache_flush_deferred_for_vol (uint16_t vid) |
| Process deferred cleanup entries for a closing volume synchronously. | |
| int | dircache_has_deferred_work (void) |
| int | dircache_process_deferred_chain (void) |
| Process one hash chain from the current deferred cleanup job. | |
Variables | |
| static hash_t * | dircache |
| static unsigned int | dircache_maxsize |
| static struct deferred_cleanup | deferred_queue [MAX_DEFERRED_CLEANUPS] |
| static int | deferred_head = 0 |
| static int | deferred_tail = 0 |
| static int | deferred_count = 0 |
| static unsigned long | queue_count_max = 0 |
| static unsigned int | dircache_validation_freq = DEFAULT_DIRCACHE_VALIDATION_FREQ |
| static volatile uint64_t | validation_counter = 0 |
| static struct dircache_stat | dircache_stat |
| unsigned long long | rfork_stat_lookups = 0 |
| unsigned long long | rfork_stat_hits = 0 |
| unsigned long long | rfork_stat_misses = 0 |
| unsigned long long | rfork_stat_added = 0 |
| unsigned long long | rfork_stat_evicted = 0 |
| unsigned long long | rfork_stat_invalidated = 0 |
| size_t | rfork_stat_used_max = 0 |
| size_t | rfork_cache_used = 0 |
| size_t | rfork_cache_budget = 0 |
| size_t | rfork_max_entry_size = 0 |
| unsigned int | rfork_lru_count = 0 |
| q_t * | rfork_lru = NULL |
| struct { | |
| unsigned long long hints_received | |
| unsigned long long hints_acted_on | |
| unsigned long long hints_no_match | |
| } | cache_hint_stat |
| struct { | |
| int enabled | |
| q_t * t1 | |
| q_t * t2 | |
| q_t * b1 | |
| q_t * b2 | |
| size_t t1_size | |
| size_t t2_size | |
| size_t b1_size | |
| size_t b2_size | |
| size_t p | |
| size_t c | |
| struct { | |
| uint64_t hits_t1 | |
| uint64_t hits_t2 | |
| uint64_t ghost_hits_b1 | |
| uint64_t ghost_hits_b2 | |
| uint64_t adaptations | |
| uint64_t evictions_t1 | |
| uint64_t evictions_t2 | |
| uint64_t promotions_t1_to_t2 | |
| size_t p_min | |
| size_t p_max | |
| uint64_t p_increases | |
| uint64_t p_decreases | |
| } stats | |
| } | arc_cache |
| static hash_t * | index_didname |
| static q_t * | index_queue |
| static unsigned long | queue_count |
Directory Cache.
Cache files and directories in a LRU cache.
The directory cache caches directories and files(!). The main reason for having the cache is avoiding recursive walks up the path, querying the CNID database each time, when we have to calculate the location of e.g. directory with CNID 30, which is located in a dir with CNID 25, next CNID 20 and then CNID 2 (the volume root as per AFP spec). If all these dirs where in the cache, each database look up can be avoided. Additionally there's the element "fullpath" in struct dir, which is used to avoid the recursion in any case. Wheneveer a struct dir is initialized, the fullpath to the directory is stored there.
In order to speed up the CNID query for files too, which e.g. happens when a directory is enumerated, files are stored too in the dircache. In order to differentiate between files and dirs, we set the flag DIRF_ISFILE in struct dir.d_flags for files.
The most frequent codepatch that leads to caching is directory enumeration (cf enumerate.c):
The dircache is a LRU cache, whenever it fills up we call dircache_evict internally which removes DIRCACHE_FREE_QUANTUM elements from the cache.
There is only one cache for all volumes, so of course we use the volume id in hashing calculations.
In order to avoid cache poisoning, we store the cached entries st_ctime from stat in struct dir.ctime_dircache. Later when we search the cache we compare the stored value with the result of a fresh stat. If the times differ, we remove the cached entry and return "no entry found in cache". A elements ctime changes when
The maximum dircache size is: max(DEFAULT_DIRCACHE_SIZE, min(size, MAX_DIRCACHE_SIZE)). It is a hashtable which we use to store "struct dir"s in. If the cache get full, oldest entries are evicted in chunks of DIRCACHE_FREE.
We have/need two indexes:
Sending SIGINT to a afpd child causes it to dump the dircache to a file "/tmp/dircache.PID".
| #define get16bits | ( | d | ) |
| #define HINT_READ_BUF_SIZE |
| #define MAX_DEFERRED_CLEANUPS 5120 |
| #define MAX_HINTS_PER_CYCLE 32 |
| enum arc_list_t |
|
static |
ARC Case I: Cache hit in T1 or T2.
From paper: "Move x to MRU of T2"
Promotes entry from T1 to T2 (frequency list) on second+ access, or moves within T2 if already frequent.
| [in] | dir | Directory entry in T1 or T2 |
|
static |
ARC Case II: Ghost hit in B1.
From paper: "Adapt p = min(c, p + max(|B2|/|B1|, 1)) REPLACE(p) Move x to MRU of T2, load into cache"
B1 hit indicates we evicted this entry too soon from T1. Increase p to favor recency (grow T1 target size).
Ghost entries in this implementation are full struct dir with DIRF_ARC_GHOST flag. This function adapts p, calls REPLACE, and promotes ghost from B1 to T2 inline using queue_move_to_tail_of() for zero-allocation transition.
| [in] | ghost | Ghost entry (struct dir with DIRF_ARC_GHOST) in B1 |
|
static |
ARC Case III: Ghost hit in B2.
From paper: "Adapt p = max(0, p - max(|B1|/|B2|, 1)) REPLACE(p) Move x to MRU of T2, load into cache"
B2 hit indicates we evicted this entry too soon from T2. Decrease p to favor frequency (grow T2 target size).
Ghost entries in this implementation are full struct dir with DIRF_ARC_GHOST flag. This function adapts p, calls REPLACE, and promotes ghost from B2 to T2 inline using queue_move_to_tail_of() for zero-allocation transition.
| [in] | ghost | Ghost entry (struct dir with DIRF_ARC_GHOST) in B2 |
|
static |
ARC Case IV: Complete miss (not in any list).
From paper: "case (i): |L1| = c if |T1| < c then delete LRU of B1, REPLACE(p) else delete LRU of T1, remove from cache case (ii): |L1| < c and |L1| + |L2| ≥ c if |L1| + |L2| = 2c then delete LRU of B2 REPLACE(p) Put x at MRU of T1, load into cache"
Adds new entry to T1 (recency list).
| [in] | dir | New directory entry to insert |
|
static |
Destroy ARC cache structures.
|
static |
Ensure ghost directory has room for one more entry.
Maintains the ARC invariant: B1 + B2 ≤ c
Called before operations that will add a ghost entry to B1 or B2. If ghost directory is at or over capacity, deletes the LRU ghost from the specified target list to make room.
This handles the edge case where entries are removed from the cache via dircache_remove() (bypassing ghost lists), causing the ghost directory to stay at capacity while the cache shrinks.
| [in] | target_list | Which ghost list will receive new entry (ARC_B1 or ARC_B2) |
|
static |
Evict a victim from a cache queue to the corresponding ghost list.
Moves victim's queue node from src to dst, sets ghost flag, and updates all ARC size counters and stats.
| [in] | victim | Directory entry to evict (must have valid qidx_node) |
| [in] | src | Source cache queue (T1 or T2) |
| [in] | dst | Destination ghost queue (B1 or B2) |
| [in] | dst_list | ARC_B1 or ARC_B2 (determines which counters to update) |
| [in] | fallback | true if this is a cross-queue fallback eviction (for logging) |
Find an evictable victim in a cache queue, skipping curdir.
Only searches T1 and T2 (the real cache queues). B1/B2 are ghost lists with full data.
Tries the LRU entry first; if it's curdir, tries the second-LRU.
| [in] | queue | The ARC queue to search (T1 or T2) |
| [in] | queue_size | Number of entries in the queue |
|
static |
Initialize ARC cache structures.
| [in] | cache_size | Total cache size (c) |
|
static |
ARC REPLACE subroutine (from paper Figure 1).
Evicts one entry from T1 or T2 and moves it to the corresponding ghost list (B1 or B2). Decision based on:
If the preferred queue only contains curdir (which must never be evicted), falls back to the other queue. Returns -1 only when both queues are exhausted, allowing the caller to proceed with a temporarily over-capacity cache that self-heals on the next eviction cycle.
From paper: "if |T1| ≥ 1 and ((x ∈ B2 and |T1| = p) or |T1| > p): Move LRU of T1 to MRU of B1, remove from cache else: Move LRU of T2 to MRU of B2, remove from cache"
| [in] | in_b2 | 1 if incoming request is in B2, 0 otherwise |
|
static |
Verify ARC invariants (debug mode).
Checks that ARC list sizes satisfy all constraints from the paper. Called after each operation in debug builds.
create struct dir from struct path
Add a struct dir to the cache and its indexes. Supports both LRU mode (legacy) and ARC mode.
| [in] | vol | pointer to volume |
| [in] | dir | pointer to parent directory |
| void dircache_dump | ( | void | ) |
Dump dircache to /tmp/dircache.PID.
|
static |
Remove a fixed number of (oldest) entries from the cache and indexes.
The default is to remove the 256 oldest entries from the cache.
| void dircache_flush_deferred_for_vol | ( | uint16_t | vid | ) |
Process deferred cleanup entries for a closing volume synchronously.
| int dircache_has_deferred_work | ( | void | ) |
| int dircache_init | ( | int | reqsize | ) |
Initialize the dircache and indexes.
This is called in child afpd initialization. The maximum cache size will be max(DEFAULT_DIRCACHE_SIZE, min(size, MAX_DIRCACHE_SIZE)). It initializes a hashtable which we use to store a directory cache in. It also initializes two indexes:
| [in] | reqsize | requested maximum size from afp.conf |
| int dircache_process_deferred_chain | ( | void | ) |
Process one hash chain from the current deferred cleanup job.
Called by idle worker. Walks a single hash chain, collects up to DEFERRED_CHAIN_BATCH matching children, and removes+frees them via dir_remove_and_free(). Entries matching curdir are skipped.
| void dircache_promote | ( | struct dir * | dir | ) |
Promote a cache entry to signal recency.
Dispatches based on cache mode:
ARC mode (arc_list membership): T1/T2: arc_case_i() — move to MRU of T2 B1: arc_case_ii_adapt_and_replace() — promote ghost to T2 B2: arc_case_iii_adapt_and_replace() — promote ghost to T2
LRU mode: Move entry to MRU position of the LRU queue, so recently accessed entries are evicted last. Prevents actively-used entries from being evicted.
| [in,out] | dir | Cache entry to promote (required) |
Re-insert entry into DID/name index after key change.
Called by dir_modify() after updating d_pdid / d_u_name. The caller MUST have already removed the entry from the DIDNAME_INDEX via dircache_remove(vol, dir, DIDNAME_INDEX) before calling this.
| [in] | vol | Volume (required) |
| [in] | dir | Entry with updated d_pdid / d_u_name (required) |
Remove an entry from the dircache.
Callers outside of dircache.c should call this with flags = QUEUE_INDEX | DIDNAME_INDEX | DIRCACHE.
Remove all child entries of a directory from the dircache.
When a directory is renamed or moved, the full paths stored in the dircache become invalid for all child entries of the renamed dir. This function prunes orphaned child dircache entries of given dir. CNID entries use parent DIDs and name, and requre recursion to get the full path, therefore parent changes do not invalidate the CNIDs.
| [in] | vol | volume |
| [in] | dir | parent directory whose children should be removed |
Enqueue a deferred dircache_remove_children operation.
O(1) enqueue — the idle worker performs the actual hash scan during poll() idle periods. Falls back to synchronous removal if the worker is not active or the queue is full.
| void dircache_report_invalid_entry | ( | struct dir * | dir | ) |
Report that a cache entry was invalid when actually used.
This function should be called when a cached directory entry that was returned without validation (for performance) turns out to be invalid when actually accessed (e.g., file doesn't exist, has been modified, etc). This helps track the effectiveness of the validation frequency setting.
| [in] | dir | The directory entry that was found to be invalid |
| void dircache_reset_validation_counter | ( | void | ) |
Reset validation counter for consistent testing.
Resets the global validation counter to ensure predictable validation patterns between test runs or configuration changes.
| void dircache_rfork_shutdown | ( | void | ) |
Shutdown the rfork cache — free remaining RFork LRU nodes and sentinel.
Called from the child shutdown path after log_dircache_stat(). Frees the qnodes (rfork data buffers are reclaimed by exit()) and the LRU sentinel.
Search the dircache via a CNID for a directory.
Found cache entries are expunged if both the parent directory st_ctime and the objects st_ctime are modified. This func builds on the fact, that all our code only ever needs to and does search the dircache by CNID expecting directories to be returned, but not files. Thus (1) if we find a file for a given CNID we (1a) remove it from the cache (1b) return NULL indicating nothing found (2) we can then use d_fullpath to stat the directory
| [in] | vol | pointer to struct vol |
| [in] | cnid | CNID of the directory to search |
| struct dir * dircache_search_by_name | ( | const struct vol * | vol, |
| const struct dir * | dir, | ||
| char * | name, | ||
| size_t | len ) |
Search the cache via did/name hashtable.
Found cache entries are expunged if both the parent directory st_ctime and the objects st_ctime are modified.
| [in] | vol | volume |
| [in] | dir | directory |
| [in] | name | name (server side encoding) |
| [in] | len | strlen of name |
| int dircache_set_validation_params | ( | unsigned int | freq | ) |
Set directory cache validation frequency.
Allows runtime configuration of cache validation behavior for performance tuning. Lower validation frequency improves performance but may delay detection of external filesystem changes.
| [in] | freq | validation frequency (1 = validate every access, 100 = every 100th access) |
|
inlinestatic |
|
inlinestatic |
|
inlinestatic |
|
inlinestatic |
|
inlinestatic |
| void log_dircache_stat | ( | void | ) |
Log dircache statistics.
Includes hit ratio percentage for monitoring cache effectiveness, validation-specific metrics to monitor performance impact of the optimization changes, and username for tracking per-user stats. Shows both expunged (caught by validation) and invalid_on_use (missed by validation).
| void process_cache_hints | ( | AFPObj * | obj | ) |
Process cross-process dircache invalidation hints.
Called from the DSI command loop after each AFP command completes. Uses direct hash_lookup() on the CNID hash table to support BOTH files (DIRF_ISFILE) and directories — dircache_search_by_did() is unsuitable because it actively removes file entries.
Dispatches on hint type:
|
static |
Determine if cache entry should be validated against filesystem.
Uses probabilistic validation to reduce filesystem calls while still detecting external changes. Internal netatalk operations use explicit cache invalidation via dir_remove() calls, so frequent validation is only needed to detect external filesystem changes.
| uint64_t adaptations |
Number of p adjustments
| struct { ... } arc_cache |
| q_t* b1 |
Ghost entries from T1 (full struct dir with DIRF_ARC_GHOST)
| size_t b1_size |
| q_t* b2 |
Ghost entries from T2 (full struct dir with DIRF_ARC_GHOST)
| size_t b2_size |
| size_t c |
Total cache size
| struct { ... } cache_hint_stat |
Cache hint processing statistics — logged by log_dircache_stat()
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
| int enabled |
0 = LRU mode, 1 = ARC mode
| uint64_t evictions_t1 |
Evictions from T1
| uint64_t evictions_t2 |
Evictions from T2
| uint64_t ghost_hits_b1 |
Ghost hits in B1
| uint64_t ghost_hits_b2 |
Ghost hits in B2
| unsigned long long hints_acted_on |
Hints that matched a local cache entry
| unsigned long long hints_no_match |
Hints for entries not in local cache (zero cost)
| unsigned long long hints_received |
Total hints read from pipe
| uint64_t hits_t1 |
Hits in T1
| uint64_t hits_t2 |
Hits in T2
|
static |
|
static |
| size_t p |
Target size for T1
| uint64_t p_decreases |
Times p decreased
| uint64_t p_increases |
Times p increased
| size_t p_max |
Min/max p values seen
| size_t p_min |
| uint64_t promotions_t1_to_t2 |
Promotions from T1 to T2
|
static |
|
static |
Peak cached entries reached this session (shared by LRU and ARC)
| size_t rfork_cache_budget = 0 |
| size_t rfork_cache_used = 0 |
| unsigned int rfork_lru_count = 0 |
| size_t rfork_max_entry_size = 0 |
| unsigned long long rfork_stat_added = 0 |
| unsigned long long rfork_stat_evicted = 0 |
| unsigned long long rfork_stat_hits = 0 |
| unsigned long long rfork_stat_invalidated = 0 |
| unsigned long long rfork_stat_lookups = 0 |
| unsigned long long rfork_stat_misses = 0 |
| size_t rfork_stat_used_max = 0 |
| struct { ... } stats |
| q_t* t1 |
Recent entries (cached)
| size_t t1_size |
| q_t* t2 |
Frequent entries (cached)
| size_t t2_size |
|
static |