Directory Cache. More...
#include <assert.h>#include <errno.h>#include <signal.h>#include <stdint.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 "pfd_cache.h"#include "hash.h"#include "idle_worker.h"Data Structures | |
| struct | deferred_cleanup |
| struct | dircache_stat |
| struct | [struct].cache_hint_stat |
| struct | [struct].arc_cache |
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 | dircache_defer_free (struct dir *dir) |
| Hash-removed entry: defer its free to end-of-request. | |
| void | dircache_purge_vol (const struct vol *vol) |
| Release every cache entry belonging to a closing volume. | |
| qnode_t * | arc_ghost_trim_candidate (q_t *q, const struct dir *skip) |
| Pick the ghost to release from a list at capacity. | |
| static void | arc_ensure_ghost_capacity (arc_list_t target_list) |
| Ensure the ghost lists have 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_make_room (void) |
| ARC Case IV, eviction half: make room for a complete miss. | |
| static void | arc_case_iv_insert (struct dir *dir) |
| ARC Case IV, insertion half: "Put x at MRU of T1, load into cache". | |
| static hash_val_t | hash_didname (const void *p) |
| static int | hash_comp_didname (const void *k1, const void *k2) |
| static unsigned int | validation_freq (void) |
| Validation frequency in effect. | |
| 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. | |
| static uint8_t | gone_event_for (const struct dir *dir) |
| static void | validation_expunge_and_hint (const struct vol *vol, struct dir *cdir) |
| Validation discovered the entry gone: expunge + hint siblings. | |
| static int | validation_refresh_and_hint (const struct vol *vol, struct dir *cdir, struct stat *st) |
| Validation saw an ino/ctime change: refresh in-place + hint. | |
| struct dir * | dircache_lookup_parent (const struct vol *vol, cnid_t did) |
| Plain index probe for pfd_cache parent resolution. | |
| 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. | |
| static void | dircache_expunge_duplicate (const struct vol *vol, struct dir *dup, int *took_curdir) |
| Retire a cached entry that a pending insert is about to supersede. | |
| int | dircache_add (const struct vol *vol, struct dir *dir) |
| create struct dir from struct path | |
| static void | dircache_mismatch_panic (const char *index_name, const struct dir *dir, const void *nodenode, const struct dir *found) |
| A stored index node does not name its entry: unrecoverable. | |
| 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. | |
| unsigned int | dircache_resolve_size (int reqsize) |
| Resolve a requested dircache size to the effective maximum. | |
| 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. | |
| 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. | |
| static void | deferred_job_kill (int idx) |
| Free a job slot without completing it (volume gone / covered). | |
| static void | deferred_compact_head (void) |
| Advance deferred_head past dead slots, keeping count consistent. | |
| void | dircache_flush_deferred_for_vol (uint16_t vid) |
| Process deferred cleanup entries for a closing volume synchronously. | |
| int | dircache_has_deferred_work (void) |
| static int | path_is_strict_child (const char *path, size_t len, const char *parent_path, size_t parent_len) |
| Test path strictly below parent: byte-prefix at a '/' boundary. | |
| static void | deferred_cover_set (const struct deferred_cleanup *done, unsigned int scan_seq) |
| Remember a completed scan as the covering memo. | |
| static int | deferred_cover_drops (const struct deferred_cleanup *j) |
| Test whether a popped job is covered by the memoized scan. | |
| static void | deferred_compact_tail (void) |
| Compact deferred_tail backwards past dead slots. | |
| static int | deferred_batch_cmp (const void *a, const void *b) |
| qsort comparator for batch members: vid, then path bytes. | |
| static int | deferred_batch_match (uint16_t vid, const char *ep, size_t elen) |
| Test whether an entry path is covered by a batch member. | |
| static int | deferred_batch_start (void) |
| Start a flat batch scan over every live pending job. | |
| static void | deferred_batch_complete (void) |
| Finish the flat batch scan: retire every pre-batch member. | |
| static int | deferred_batch_chain (void) |
| Scan one hash chain for the flat batch: purge every entry whose unique candidate ancestor is a batch member. | |
| int | dircache_process_deferred_chain (void) |
| Process one unit of deferred cleanup. | |
Variables | |
| static hash_t * | dircache |
| static unsigned int | dircache_maxsize |
| static uint32_t | vid_entry_count [UINT16_MAX+1] |
| static unsigned int | deferred_seq = 0 |
| static unsigned int | deferred_scan_started_seq = 0 |
| static int | deferred_active = -1 |
| static unsigned int | deferred_depth_min = 0 |
| static unsigned int | deferred_depth_max = 0 |
| static char * | deferred_cover_path = NULL |
| static size_t | deferred_cover_len = 0 |
| static uint16_t | deferred_cover_vid = 0 |
| static unsigned int | deferred_cover_seq = 0 |
| static int | deferred_batch_active = 0 |
| static unsigned int | deferred_batch_seq = 0 |
| static unsigned int | deferred_batch_depth = 0 |
| static hashcount_t | deferred_batch_chain_idx = 0 |
| static int | deferred_batch_n = 0 |
| static struct deferred_cleanup | deferred_queue [MAX_DEFERRED_CLEANUPS] |
| static int | deferred_batch_idx [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 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 |
| static struct | cache_hint_stat |
| static struct | arc_cache |
| static const struct dir * | arc_promoting_ghost = NULL |
| 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 resolves from the requested size: unset or below MIN_DIRCACHE_SIZE uses DEFAULT_DIRCACHE_SIZE, in-range values round up to the next power of two, larger requests clamp to MAX_DIRCACHE_SIZE. It is a hashtable which we use to store "struct dir"s in. If the cache gets full, oldest entries are evicted in chunks of DIRCACHE_FREE_QUANTUM.
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, insertion half: "Put x at MRU of T1, load into cache".
| [in] | dir | New directory entry, already in both hash indexes |
|
static |
ARC Case IV, eviction half: make room for a complete miss.
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)"
Runs before the new entry's hash inserts: whenever the dircache holds 2c entries this deletes one from the hash, so hash_insert() never sees a full table (its nodecount < maxcount assert holds).
|
static |
Destroy ARC cache structures.
|
static |
Ensure the ghost lists have 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 the ghost lists are 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 lists to stay at capacity while the cache shrinks.
A ghost mid-promotion is skipped, leaving the ghost lists one over capacity until that promotion moves it out.
| [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 |
Pick the ghost to release from a list at capacity.
LRU first, skipping the excluded entry; a single-node list is both MRU and LRU, so MRU ordering alone cannot exclude it.
| [in] | q | Ghost queue (B1 or B2) |
| [in] | skip | Entry that must not be selected (ghost mid-promotion), or NULL |
Non-static so the unit tests can drive selection directly
|
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.
|
static |
Scan one hash chain for the flat batch: purge every entry whose unique candidate ancestor is a batch member.
|
static |
qsort comparator for batch members: vid, then path bytes.
|
static |
Finish the flat batch scan: retire every pre-batch member.
Mid-batch enqueues (seq >= batch seq) stay pending — their entries may sit in chains the batch already passed.
|
static |
Test whether an entry path is covered by a batch member.
The candidate ancestor is unique at uniform depth: the entry path truncated at its (batch_depth+1)-th slash. Binary search over the sorted member index; byte comparisons only.
|
static |
Start a flat batch scan over every live pending job.
|
static |
Advance deferred_head past dead slots, keeping count consistent.
|
static |
Compact deferred_tail backwards past dead slots.
LIFO consumption retires slots at the tail; head compaction still handles slots killed by the volume flush.
|
static |
Test whether a popped job is covered by the memoized scan.
Covered iff enqueued before that scan started (serial-number compare: the unsigned seq difference exceeds half the counter space iff the job predates the scan, fully defined across wraparound), same volume, and its byte path equals or sits strictly below the memoized path at a '/' boundary.
|
static |
Remember a completed scan as the covering memo.
|
static |
Free a job slot without completing it (volume gone / covered).
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 |
|
static |
Hash-removed entry: defer its free to end-of-request.
Caller must have removed the entry from both hash indexes and every ARC/LRU queue. Deferring keeps pointers handed out earlier in this request valid (consumers gate on d_did != CNID_INVALID) — ARC ghost eviction can reach entries that WERE returned this request, so an inline free here could dangle a held pointer. On qnode OOM (effectively unreachable: the kernel OOM killer fires first) leak the entry instead; dir_remove makes the same choice.
| 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.
|
static |
Retire a cached entry that a pending insert is about to supersede.
Not dir_remove(): its curdir recovery calls dirlookup(), which re-enters dircache_add() and re-publishes the key the caller is clearing. curdir is handed back instead, for the caller to re-point once its entry is published.
| [in] | vol | Volume the entry belongs to |
| [in] | dup | Entry to retire |
| [out] | took_curdir | set if the retired entry was curdir |
| 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. Unset or below-minimum sizes use DEFAULT_DIRCACHE_SIZE, in-range sizes round up to the next power of two, oversize requests clamp to 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 |
Plain index probe for pfd_cache parent resolution.
Never validates, never mutates, never promotes. Filters ARC ghosts (their identity fields are frozen and unvalidated — treating one as a live parent would let the pfd sync-check pass against stale identity) and file entries (a parent must be a directory).
|
static |
A stored index node does not name its entry: unrecoverable.
Every node is created with its entry as data and freed only through the entry's own removal, so a mismatch means memory corruption or a foreign delete. Freeing what a table still points at cannot be survived.
| int dircache_process_deferred_chain | ( | void | ) |
Process one unit of deferred cleanup.
Called by idle worker under a validated grant. Jobs are consumed LIFO: AFP deletes bottom-up, so a tree's covering root-most job arrives last and its single scan purges the whole subtree; the covering memo then drops the tree's remaining jobs in O(1) as they pop. When every pending job sits at one depth, one merged batch scan serves all of them. Every removal re-checks iw_can_work; scans resume via their chain cursors across granted cycles.
| 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) |
| void dircache_purge_vol | ( | const struct vol * | vol | ) |
Release every cache entry belonging to a closing volume.
Nothing else reclaims by volume, so entries left behind are found as valid when the vid is reused. DIRCACHE_NOSHRINK keeps the scan valid: each entry deletes only its own just-returned node and the table never rehashes mid-scan.
Nothing is exempt: closevol() frees v_root next, so an entry kept back would outlive its volume under a reusable vid. A curdir still inside the volume is dropped to rootParent rather than left cached.
| [in] | vol | Volume being closed (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.
Deletes the entry's own stored nodes, so removal can never cost another entry its node. A NULL node means the entry is not in that index and it is skipped; a node naming a different entry panics before anything is mutated or followed.
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.
Removes as it scans: DIRCACHE_NOSHRINK deletes the entry's own just-returned node without rehashing the table, and the free is deferred, so nothing the scan is walking moves. dir_remove() cannot be used here — its curdir recovery calls dirlookup(), and an insert mid-scan can grow the table. A removed curdir is re-resolved once the scan is over instead: a rename keeps the CNID, so the lookup rebuilds the entry on the new path.
| [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.
| unsigned int dircache_resolve_size | ( | int | reqsize | ) |
Resolve a requested dircache size to the effective maximum.
Unset or below-minimum sizes use DEFAULT_DIRCACHE_SIZE, in-range sizes round up to the next power of two, oversize requests clamp to MAX_DIRCACHE_SIZE.
| [in] | reqsize | requested maximum size from afp.conf |
| 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 |
|
static |
|
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).
|
static |
Test path strictly below parent: byte-prefix at a '/' boundary.
The boundary byte check rejects sibling prefixes ("/A/BC" under "/A/B").
| 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.
Validation discovered the entry gone: expunge + hint siblings.
One pipe message replaces each sibling re-discovering the change by its own full-path stat. The hint reports evidence (gone); a same-named recreate is harmless — hints only invalidate, receivers rebuild.
|
static |
Validation frequency in effect.
Read live: the coherency defaults are applied per volume, after dircache_init(). The single reader for the validation decision and the statistics, so both always report the same frequency.
|
static |
Validation saw an ino/ctime change: refresh in-place + hint.
Hints: an inode change (object replaced) broadcasts REFRESH under the entry's current DID and, when dir_modify re-keyed it, under the old DID too — siblings cache the object under the old key. A ctime-only change sends nothing: fork writes advance ctime on every flush, and broadcasting those would rebuild sibling AD/rfork caches per write; pure metadata changes propagate via each sibling's own validation.
| struct [struct].arc_cache arc_cache |
| struct [struct].cache_hint_stat cache_hint_stat |
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
|
static |
|
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 |
|
static |
|
static |