Retention cleanup on a 400k-file spool
/var/spool/incoming on a RHEL 9 host receives about 400,000 files a day from an external feed. The sender controls the names: some contain spaces or single quotes, a few begin with a hyphen (one is literally named -rf), and the sender creates month-named subdirectories. Retention rule: delete regular files older than 30 days, never delete anything matching *.keep, never touch the index directory or anything under it, and never follow the symlinks in latest/ (they point at the feed server's NFS export). Cleanup runs from cron as user spool at 02:15. The current script is:
cd /var/spool/incoming && rm -f ls -t | tail -n +100000
1. Enumerate what is wrong with the current one-liner. Which of these faults are cosmetic and which cause data loss or failed runs?
Read the worked answer
The command substitution is expanded unquoted, so its output is split on IFS and then each word undergoes filename generation before rm sees it:
- Data loss / wrong targets. A name containing a space becomes two arguments; a name containing
*or?expands against the current directory; the file literally named-rfis parsed as options.rm -fswallows the error for the harmless cases and quietly deletes the wrong things for the rest. - Failed runs. 400k names in one argument vector blows past
ARG_MAX(Argument list too long), andlson that directory is slow enough to overlap the next cron run. - Wrong policy.
ls -t | tail -n +100000means 'keep the newest 99,999 entries' - retention by count, not by age. A heavy feed day deletes too little; a quiet day deletes files that are hours old. It also matches directory names, and it sees only the top level, so the month subdirectories are never considered. - Cosmetic but real.
-fmasks failures, so cron's mail is silent and nobody learns the job is doing nothing.
Every fault above is a correctness problem, not a style problem. The only defensible pieces are cd as a guard and running as the unprivileged spool user, which limits blast radius.
2. Write the find invocation and justify each predicate - especially -mtime +30 versus -mtime 30, and the interaction of -delete with -prune.
Read the worked answer
find /var/spool/incoming -xdev -type f -mtime +30 \
! -name '*.keep' \
! -path '/var/spool/incoming/index' \
! -path '/var/spool/incoming/index/*' \
-delete
Justification:
-type frestricts to regular files, so directories and the symlinks underlatest/are never candidates. find does not follow symlinks by default (-P), so the NFS export is not traversed; never add-Lhere.-xdevstops at a filesystem boundary, in case anything is mounted inside the spool.-mtime +30is not '30 days or older'. GNU find truncates the age to whole 24-hour periods and+30means that truncated value is greater than 30 - i.e. 31 days or more. A file 30 days and 20 hours old is not matched. If the policy means an exact 30-day boundary, use-mmin +43200. Also note that a file with a future mtime never matches+30.-prunecannot be used with-delete.-deleteimplies-depth, and under-depthfind processes a directory's contents before the directory, so-prunehas no effect. Excluding by-pathwith a negation works in both modes.- Negated
*.keeprather than-prune: same reason, and it also catches keep-files inside the month subdirectories. - The leading-hyphen names are safe. find emits paths with the starting-point prefix (
/var/spool/incoming/-rf), so no argument can be read as an option. That is a property of find, not ofrm; if you must pipe torm, use-print0 | xargs -0 rm --.
3. The job has run for a week. `df` shows the spool filesystem full while find matches almost nothing. What do you investigate, and how would you have validated the selector before enabling cron?
Read the worked answer
Space that find cannot see comes from one of a few places; check in this order:
- Deleted-but-open files.
lsof +L1 /var/spool/incoming(orlsof | grep -i deleted). An ingest process still holding an unlinked 40 GB file keeps the blocks allocated;ducannot see it,dfcounts it. Fix by restarting the holder, not by touching the spool. - Future mtimes. The sender controls timestamps; a clock running ahead produces mtimes in the future, which
-mtime +30never matches and-mtime -1happily does. Confirm withfind ... -newermt 'now' -printf '%TY-%Tm-%Td %p\n' | head. - Files below a mount or inside a directory the pattern excludes.
mount | grep spool, andfind /var/spool/incoming -xdev -type d -newer /var/spool/incoming -maxdepth 2 | headto spot unexpected tree shapes.
Validation before enabling cron: run the identical find with -print instead of -delete, log a count (... -print | wc -l) and a sample, compare against the previous day's count, and only then switch the predicate. Keep the count in the cron output so a silent zero or a million is visible, and keep spool as the executing user so a mistake cannot reach outside the spool.