
چرا یک دستور mv ساده میتواند جلوی چهار بار پردازش فایلها را بگیرد؟
وقتی چند فرآیند همزمان برای پردازش فایلهای یک صف برنامهریزی میشوند، بدون قفلگذاری برخورد و پردازش تکراری رخ میدهد. این مقاله با بررسی یک باگ واقعی، راهحلی اتمی و ساده با دستور mv در Bash ارائه
نکات کلیدی
- استفاده از launchd با یک پوشهی مشترک برای صف فایل، بدون قفلگذاری صریح، به برخورد و پردازش تکراری میانجامد.
- انتقال فایل از صف به پوشهی پردازش با `mv`، بهدلیل اتمی بودن `rename` در سطح سیستمعامل، سادهترین و مطمئنترین راه قفلگذاری در اسکریپتهای Bash است.
- در محیط عملیاتی، بازیابی فایلهای راکد، تلهی EXIT برای تمیزکاری و تنظیم صریح PATH را اضافه کنید تا از گیرکردن سیستم جلوگیری شود.
در ۱۷ سپتامبر ۲۰۲۶، یک سیستم خودکار مبتنی بر Claude Code بهدلیل خطای usage_limit_exceeded بهمدت یک روز کامل از کار افتاد. نتیجه؟ انباشته شدن ۵۷ فایل JSON در یک دایرکتوری و پس از بازگشت سرویس، راهاندازی همزمان پنج فرآیند پردازشگر که باعث شد بعضی فایلها در فاصلهای یکدقیقهای چهار بار پردازش شوند. این یک تصادف نبود؛ خروجی حتمی یک طراحی ناقص بود: وقتی launchd را با یک پوشهی مشترک بهعنوان صف ترکیب میکنید، برخورد قطعی است مگر اینکه بهطور صریح قفلگذاری پیادهسازی شود.
مشکل ساختاری، نه احتمالی
launchd در macOS وظیفهاش اجرای اسکریپتها در زمانهای مشخص است؛ نه نظارت بر پایان اجرای قبلی و نه مدیریت درگیری منابع. نمونهای از تنظیمات در فایل com.lily.codex-note-funnel.plist:
<key>StartCalendarInterval</key>
<array>
<dict>
<key>Hour</key>
<integer>10</integer>
<key>Minute</key>
<integer>40</integer>
</dict>
<dict>
<key>Hour</key>
<integer>16</integer>
<key>Minute</key>
<integer>40</integer>
</dict>
</array>
در شرایط عادی، ورود فایلها به صف تدریجی است و زمانبندی فرآیندها کمی با هم اختلاف دارد؛ بههمین دلیل برخورد نادر است. اما یک وقفهی طولانی مثل قطعی سرویس همهچیز را عوض میکند: با بازگشت سرویس، همهی فرآیندها همزمان بالا میآیند و به اولین فایلهای داخل done/ هجوم میبرند. راهحلهای سطحی مثل «افزایش فاصلهی زمانبندی» یا «چک کردن بیشتر لاگها» فقط شدت مشکل را کم میکنند؛ ریشهی آن را از بین نمیبرند.
چرا راهحلهای معمول شکست میخورند؟
قفل فایل ساده (Lock File): ایدهی اولیه ایجاد یک فایل قفل است:
LOCKFILE=~/dev/note-autolike/.processing.lock
if [! -f "$LOCKFILE" ]; then
touch "$LOCKFILE"
process_file "$json"
rm "$LOCKFILE"
fi
این روش دو مشکل بزرگ دارد:
- پنجرهی TOCTOU (Time of Check to Time of Use): بین بررسی وجود فایل (
if [! -f ]) و ایجاد آن (touch) فاصلهای وجود دارد که در همین فاصله فرآیند دیگر میتواند وارد شود و هر دو، قفل را بسازند. - قفلهای باقیمانده: اگر فرآیند وسط کار کرش کند، فایل قفل پاک نمیشود و اجراهای بعدی بهاشتباه فکر میکنند پردازش در جریان است و متوقف میشوند.
قفلگذاری با flock: استفاده از flock در این سناریو نتیجه نمیدهد، چون launchd هر بار یک فرآیند تازه ایجاد میکند و توصیفگرهای فایل به اجراهای بعدی به ارث نمیرسند.
بررسی PID: یک اسکریپت موجود در سیستم (autolike-plist-reconcile.sh) با چک کردن PID از اجرای همزمان جلوگیری میکند:
pid=$(launchctl list | awk -v l="$L" '$3==l{print $1}')
if [ -n "$pid" ] && [ "$pid"!= "-" ]; then
echo "[$L در حال اجراست، رد شد]"
continue
fi
این روش در جای خودش مفید است، اما هنوز بین «بررسی» و «شروع پردازش» فاصله دارد و برای تصاحب انحصاری صف کافی نیست.
راهحل اتمی: تصاحب با mv
کلید کار، یک عملیات اتمی (atomic) در سطح هسته است. mv (یا rename در سطح سیستمعامل) روی یک فایلسیستم واحد چنین ویژگیای دارد.
چطور کار میکند؟ هسته، تغییر نام/جابجایی یک فایل داخل یک فایلسیستم را در یک فراخوان سیستمی غیرقابل تقسیم انجام میدهد. حتی اگر دو فرآیند دقیقا همزمان این دستور را بزنند:
mv done/job_001.json processing/job_001.json
دقیقا یکی موفق میشود و دیگری با خطای «چنین فایلی وجود ندارد» روبهرو میشود. هیچ فاصلهای بین «بررسی» و «تصاحب» وجود ندارد.
پیادهسازی عملی در اسکریپت
کدی که در ابتدای اسکریپت پردازش (run-codex-funnel.sh) قرار میگیرد:
#!/bin/bash
set -uo pipefail
DONE_DIR=~/dev/note-autolike/done
PROCESSING_DIR=~/dev/note-autolike/processing
mkdir -p "$PROCESSING_DIR"
# انتخاب یک فایل از صف
TARGET=$(ls "$DONE_DIR"/*.json 2>/dev/null | head -1)
[ -z "$TARGET" ] && exit 0 # اگر صف خالی بود، خروج عادی
BASENAME=$(basename "$TARGET")
# ─── هسته راهحل ─────────────────────────────────────────
# تصاحب اتمی. شکست یعنی فرآیند دیگری فایل را گرفته است.
mv "$TARGET" "$PROCESSING_DIR/$BASENAME" 2>/dev/null || exit 0
# ────────────────────────────────────────────────────────
WORKING="$PROCESSING_DIR/$BASENAME"
# از اینجا به بعد، فقط این فرآیند حق پردازش WORKING را دارد.
نکات کلیدی:
2>/dev/null: جلوی پیام «No such file or directory» در خروجی خطا را میگیرد.|| exit 0: اگرmvشکست بخورد (یعنی فایل را فرآیند دیگر برداشته)، اسکریپت با کد خروجی موفق (0) تمام میشود تاThrottleIntervalدر launchd فعال نشود.- این روش فقط وقتی اتمی است که مبدا و مقصد (
done/وprocessing/) روی یک فایلسیستم واحد باشند. روی mount pointهای مختلف (مثلا NFS) اتمی بودن از بین میرود.
لایههای تکمیلی برای محیط عملیاتی
برای دنیای واقعی، باید لایههایی برای بازیابی و تمیزکاری داشته باشید:
۱. بازیابی فایلهای راکد (Stale Files):
اگر اسکریپت با kill -9 متوقف شود، فایل در processing/ میماند. در آغاز هر اجرا، فایلهای قدیمیتر از یک ساعت را به صف برگردانید:
for stale in "$PROCESSING_DIR"/*.json; do
[ -f "$stale" ] || break
age=$(( $(date +%s) - $(stat -f %m "$stale") ))
if [ "$age" -gt 3600 ]; then
mv "$stale" "$DONE_DIR/" 2>/dev/null
fi
done
۲. تلهی EXIT برای تمیزکاری:
با trap مطمئن شوید در هر حالت (موفق، خطا یا interrupt) فایل از processing/ خارج میشود:
_cleanup() {
local code=$?
if [ $code -eq 0 ]; then
mv "$WORKING" "$PROCESSED_DIR/$BASENAME" 2>/dev/null
else
mv "$WORKING" "$DONE_DIR/$BASENAME" 2>/dev/null
fi
}
trap '_cleanup' EXIT
۳. تنظیم متغیر PATH:
launchd بهطور پیشفرض PATH محدودی (/usr/bin:/bin:/usr/sbin:/sbin) دارد. اگر اسکریپت به ابزارهایی در /usr/local/bin (مثل jq، node، python3) نیاز دارد، آن را صریح تنظیم کنید:
PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
export PATH
درسهای آموختهشده و دامهای رایج
- لاگ خطای در حال رشد: اگر بعد از پیادهسازی، لاگ خطا پر از «No such file or directory» است، بررسی کنید
2>/dev/nullکنار|| exit 0را جا نینداخته باشید. - نظارت بر تکرارها: ساختار اتمی، تکرار را صفر میکند؛ اما برای تشخیص، ابزاری لازم دارید. یک دستور ساده برای یافتن فایلهای پردازششدهی تکراری:
ls ~/dev/note-autolike/processed/ | sed 's/_[0-9]*$//' | sort | uniq -d
- ترتیب نام فایلها: اگر چند فایل در یک ثانیه ساخته شوند، ترتیب لغوی
lsممکن است ناپایدار باشد و هر فرآیند «اولین» فایل متفاوتی را برگزیند. برای اطمینان، شناسهای مثل timestamp و PID را در نامها بگنجانید. - چرخش لاگ (Log Rotation): launchd لاگها را خودکار نمیچرخاند. لاگهای بزرگ میتوانند فضای دیسک را پر کنند. از ابزارهایی مثل
logrotateیا مدیریت اندازه در خود اسکریپت استفاده کنید.
در نهایت، فلسفهی پشت «mv ... || exit 0» این است که هر خطا الزاما چیز قابل لاگکردن نیست؛ گاهی یک سیگنال واضح برای اقدام بعدی است (خروج سالم). این نگاه، پایداری اسکریپتهای خودکار را بهطور محسوسی بالا میبرد.
ابزارهای مرتبط والور
منابع
این مقاله با کمک هوش مصنوعی از منابع بالا تدوین و توسط امیرعلی کریمی بازبینی شده است. قوانین محتوایی والور

بنیانگذار و سردبیر والور
امیرعلی کریمی بنیانگذار و سردبیر والور است. مقالههای والور با کمک هوش مصنوعی و بر پایهی چند منبع معتبر بینالمللی تدوین میشوند و من پیش از انتشار، هر مطلب را از نظر درستی اطلاعات، روانی متن و منابع بازبینی و تایید میکنم. منابع هر مقاله در انتهای همان صفحه فهرست شدهاند.
مطالب مرتبط
کلود کد ۲.۱.۲۷۵: پشتیبانی از AGENTS.md و بیش از ۲۰۰ اصلاحیه برای بهبود تجربه توسعه
آخرین بهروزرسانی کلود کد (۲.۱.۲۷۵) قابلیت جدید پشتیبانی از فایل AGENTS.md را بهعنوان جایگزین CLAUDE.md معرفی میکند. این آپدیت همچنین بیش از ۲۰۰ اصلاح و بهبود در ابزارهای ویرایش، مدیریت پلاگین و
هشدار مدیرعامل Anthropic: سوارم هوش مصنوعی ظرف یک سال میتواند کل اینترنت را تسخیر کند
داریو آمودی، مدیرعامل Anthropic، هشدار داده که توسعهی شتابان هوش مصنوعی خطر از دست دادن کنترل را به همراه دارد. او از احتمال شکلگیری باتنتهای خودمختار توسط سوارمهای هوش مصنوعی تا ۱۲ ماه آینده
وقتی هوشمصنوعی کد مینویسد: چطور تغییرات مخرب در API را شناسایی کنیم؟
ابزارهای هوشمصنوعی مثل GitHub Copilot و Cursor گاهی با اضافه کردن پارامترهای اختیاری یا عمومیکردن توابع، قراردادهای نرمافزار را عوض میکنند. این مقاله یک روش عملی و اسکریپت پایتون برای پیدا کردن