پرش به محتویات

عملیات

اسکریپت‌ها

همه زیر /opt/kasbbook/scripts/، همه نیازمند sudo، همه lib.sh را منبع‌گیری می‌کنند.

install.sh از هیچ تا در حال اجرا. خودتوان.
update.sh کشیدن، تست، مهاجرت، راه‌اندازی دوباره — و اگر شکست خورد، برگشت
backup.sh تخلیه، بررسی، نگه‌داشتن چهارده تای آخر
restore.sh برگرداندن یک پشتیبان، پس از پشتیبان‌گیری از چیزی که جایگزینش می‌شود
uninstall.sh برداشتن سرویس‌ها و چک‌اوت؛ --purge برای داده‌ها هم
messenger.sh افزودن، فهرست‌کردن یا برداشتن پیام‌رسان دوم

به‌روزرسانی

sudo /opt/kasbbook/scripts/update.sh

به ترتیب:

  1. اول پشتیبان — تا مسیر برگشت حتی وقتی خودِ مهاجرت خراب می‌کند وجود داشته باشد.
  2. واکشی و بازنشانی روی سرِ شاخه.
  3. نصب وابستگی‌ها.
  4. اجرای تست‌ها — پیش از مهاجرت، نه بعدش. مجموعهٔ مردود یعنی کامیت بد است، و فهمیدن این پیش از دست‌زدن به شِما خیلی ارزان‌تر است.
  5. مهاجرت.
  6. بازنویسی یونیت‌های systemd از deploy/. به همین دلیل داخل مخزن‌اند: یک بار مسیر اجراکنندهٔ ربات از apps/telegram_bot/ به apps/bot/ رفت، و فایل یونیتی که جا مانده بود به مسیری اشاره می‌کرد که دیگر وجود نداشت.
  7. راه‌اندازی دوبارهٔ هر دو سرویس.
  8. بررسی سلامت — فعال، و بدون ردپای خطا در ده ثانیهٔ آخر ژورنال.

اگر هر گامی شکست بخورد، به کامیت قبلی بازمی‌گردد، یونیت‌های قبلی را دوباره نصب می‌کند، سرویس‌ها را بالا می‌آورد، و با کد ناصفر بیرون می‌آید و می‌گوید پشتیبان کجاست.

اسکریپت به‌روزرسانی‌ای که فقط جلو می‌رود، کامیت بد را به قطعی‌ای تبدیل می‌کند که تا وقتی کسی بیدار شود ادامه دارد. آن یکی که بررسی و برگشت می‌کند، همان را به یک به‌روزرسانی ناموفق و رباتی که هنوز کار می‌کند تبدیل می‌کند.

پشتیبان‌گیری

sudo /opt/kasbbook/scripts/backup.sh

pg_dump | gzip داخل /var/backups/kasbbook/، با دسترسی ۶۰۰ و پوشهٔ ۷۰۰ — تخلیه‌ها دفترهای همه را دارند.

بررسی می‌کند نه اینکه فرض کند: gzip -t روی چیزی که نوشته، و از اینکه به فایلی زیر یک کیلوبایت بگوید پشتیبان سر باز می‌زند، چون پیام خطای تغییرمسیرداده‌شده دقیقاً همین شکلی است.

روی SQLite از .backup استفاده می‌کند، نه cp. کپی‌ای که وسط نوشتن گرفته شود دیتابیس خرابی است که تا روزی که لازم شود سالم به نظر می‌رسد.

پیش‌فرض چهارده تای آخر را نگه می‌دارد (KASBBOOK_BACKUP_KEEP)، قدیمی‌ترین اول حذف می‌شود، و همیشه فقط داخل پوشهٔ خودش.

شبانه

sudo crontab -e
0 3 * * * /opt/kasbbook/scripts/backup.sh --quiet

بیرون از سرور

پشتیبانی که روی همان دیسک دیتابیس باشد از مهاجرت بد جان سالم به در می‌برد، اما از دیسک مرده نه. جای دیگری کپی‌شان کن:

rsync -az /var/backups/kasbbook/ elsewhere:/backups/kasbbook/

بازگردانی

sudo /opt/kasbbook/scripts/restore.sh /var/backups/kasbbook/kasbbook-20260825-030000.sql.gz

طبق طراحی داده نابود می‌کند، پس دقیقاً می‌گوید چه چیزی را می‌خواهد جایگزین کند، مقصد را با رمز ماسک‌شده نشان می‌دهد، و وادارت می‌کند restore را تایپ کنی.

بعد سرویس‌ها را می‌خواباند، از چیزی که می‌خواهد رویش بنویسد پشتیبان می‌گیرد — بازگرداندن فایل اشتباه نباید غیرقابل‌جبران باشد — بازمی‌گرداند، شِما را تا کدِ در حال اجرا جلو می‌برد، و سرویس‌ها را با بررسی سلامت بالا می‌آورد.

اجرایش بدون آرگومان فهرست موجودها را نشان می‌دهد.

برگرداندن یک انتشار

update.sh هنگام شکست خودش این کار را می‌کند. برای انجام عمدی‌اش:

cd /opt/kasbbook
sudo git log --oneline -10
sudo git reset --hard <commit>
sudo ./venv/bin/pip install -q -r requirements.txt
sudo install -m 644 deploy/*.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl restart kasbbook-bot kasbbook-api

اول شِما. اگر نسخه‌ای که به آن برمی‌گردی از مهاجرتی قدیمی‌تر است، پیش از راه‌اندازی دوباره پایین بیا، وگرنه کد به ستون‌هایی برمی‌خورد که نمی‌شناسدشان:

sudo ./venv/bin/alembic downgrade -1

هر مهاجرت اینجا downgrade کارآمد دارد، و یک تست ادعا می‌کند که تا خودِ پایه برمی‌گردند — هم روی SQLite و هم روی پستگرس.

داده کد نیست. برگرداندن کد، هرچه نسخهٔ تازه‌تر نوشته را دست‌نخورده می‌گذارد. موردی که مهم است جبران تکرارشونده‌هاست: وقتی KASBBOOK_RECURRING=on باشد، ربات تراکنش واقعی ثبت می‌کند. برای برگرداندنشان دو راه هست، و فقط همین دو:

  • بازگردانی کامل پشتیبانی که update.sh پیش از به‌روزرسانی گرفته؛ این کار همهٔ جدول‌ها، از جمله دفتر و سندهای روزنامه، را با هم برمی‌گرداند؛ یا
  • برگرداندن هر تراکنش از راه خود برنامه — حذف در ربات، یا DELETE /api/v1/books/{id}/transactions/{tx}. هر دو از LedgerService رد می‌شوند، که تراکنش و سند روزنامه‌اش را با هم برمی‌دارد و دفتر را تراز نگه می‌دارد. پیام تکرارشونده‌ها به مالک، فهرست آنچه ثبت شده را دارد.

هرگز تراکنش، سند روزنامه یا سطر سند را با SQL حذف نکن، و هرگز یکی را بدون دیگری. تراکنش بدون سندش، یا سند بدون تراکنشش، دفتری است که دیگر خودش را ثابت نمی‌کند — و trial_balance() دیگر تراز نمی‌شود. برای همین است که ارتقا اول ثبت تکرارشونده را خاموش می‌کند و پیش‌نمایش را اجرا می‌کند: ارزان‌ترین برگرداندن آن است که چیزی برای برگرداندن ندارد.

سلامت

systemctl status kasbbook-bot kasbbook-api
curl -s http://127.0.0.1:8210/readyz
cd /opt/kasbbook && sudo ./venv/bin/alembic current

systemctl is-active حتی برای سرویسی که زیر Restart=always در حلقهٔ خرابی است هم می‌گوید active. به همین دلیل اسکریپت‌ها به‌جای اعتماد به آن کلمه ژورنال را دنبال ردپای خطا می‌گردند، و به همین دلیل NRestarts ناصفر روی سرویسی که کسی دستش نزده یعنی چیزی خراب است.

نسل اول

/opt/kasbbook-first-generation ربات اصلی است، غیرفعال، که به‌خاطر دیتابیس SQLite‌اش نگه داشته شده نه کدش. هیچ چیزی از آن اجرا نمی‌شود و هیچ چیزی نمی‌خواندش. وقتی مطمئن شدی هرگز آن سطرهای قدیمی را نمی‌خواهی حذفش بی‌خطر است — تصمیمی که ارزش دارد عمداً گرفته شود، نه وسط یک خانه‌تکانی.

انبارهای داده

cd /opt/kasbbook/deploy
docker compose ps
docker compose logs postgres --tail 50
docker compose restart redis

هر دو فقط به 127.0.0.1 گوش می‌دهند. از دست‌دادن ردیس قابل‌تحمل است — وضعیت گفتگو با /start بعدی بازساخته می‌شود و محدودیت نرخ به حالت درون‌پروسه برمی‌گردد. از دست‌دادن پستگرس نه؛ پشتیبان‌ها برای همین‌اند.

اجرای پیام‌رسان دوم

sudo /opt/kasbbook/scripts/messenger.sh add bale

توکن و نام‌کاربری را می‌پرسد، /opt/kasbbook/.env.bale را می‌نویسد، kasbbook-bale.service را از روی یونیت تلگرام می‌سازد، بالا می‌آوردش و بررسی می‌کند که واقعاً آمده باشد. list نشان می‌دهد این سرور چه چیزی اجرا می‌کند؛ remove <provider> یکی را می‌خواباند و حذف می‌کند و فایل توکن را نگه می‌دارد، تا حذف اشتباهی یک ری‌استارت باشد نه یک سفر دوباره به BotFather.

همان دیتابیس، و نکته هم همین است: حسابی که به هر دو پیام‌رسان وصل باشد یک مجموعه دفتر دارد.

یونیت تازه دو فایل محیط دارد — اول مشترک، بعد مال خودش — تا نشانی دیتابیس و کلید امضا در یک جا بمانند و چرخاندن کلید یک ویرایش باشد نه یکی به ازای هر پیام‌رسان.

لازم نیست جایی ثبتش کنی. UNITS از روی یونیت‌های موجود کشف می‌شود، چون آن آرایه در scripts/lib.sh جای مستندشدهٔ افزودن بود و update.sh همان فایل را بازنویسی می‌کند — ویرایش دقیقاً تا به‌روزرسانی بعدی زنده می‌ماند و بعد ناپدید می‌شد، و ربات دوم را بی‌آنکه چیزی بگوید از چرخهٔ ری‌استارت و بررسی سلامت بیرون می‌انداخت.

قاعده‌های تکرارشونده

حلقهٔ یادآور در هر نوبت، هر قاعدهٔ تکرارشونده‌ای را که موعدش رسیده ثبت می‌کند. تاریخ‌های جاافتاده را هم جبران می‌کند. تاریخی که در دورهٔ حقوقیِ از حالت «باز» گذشته باشد رد می‌شود، چون فیش‌های آن دوره بدون آن حساب شده‌اند. قاعده‌ای که سازنده‌اش دیگر نمی‌تواند در دفتر ثبت کند متوقف می‌شود. مالک دفتر یک پیام می‌گیرد با فهرست هرچه دیر ثبت شد، رد شد یا متوقف شد.

ارتقا از انتشاری که آن‌ها را ثبت نمی‌کرد. پیش از این انتشار هیچ‌چیز ثبت را صدا نمی‌زد، پس نصبِ موجود عقب‌افتادگی دارد. هر قاعده روی تاریخ شروعش مانده است و اولین نوبت، هر تاریخ جاافتاده در دورهٔ باز را ثبت می‌کند، تا ۴۰۰ بار برای هر قاعده. برای دیدنش پیش از وقوع:

# ۱. پیش از آن‌که به‌روزرسانی ربات را دوباره راه بیندازد، ثبت را نگه دار
echo 'KASBBOOK_RECURRING=off' | sudo tee -a /opt/kasbbook/.env
sudo bash /opt/kasbbook/scripts/update.sh

# ۲. دقیقاً ببین اولین اجرا چه ثبت می‌کند (اجرای واقعی است و برگردانده می‌شود)
sudo bash -c 'set -a; . /opt/kasbbook/.env; set +a; cd /opt/kasbbook && ./venv/bin/python apps/bot/recurring_preview.py'

# ۳. هر قاعده‌ای را که نباید جبران شود متوقف یا حذف کن، بعد ثبت را روشن کن
sudo sed -i 's/^KASBBOOK_RECURRING=off$/KASBBOOK_RECURRING=on/' /opt/kasbbook/.env
sudo systemctl restart 'kasbbook-bot*'

وقتی ثبت خاموش است چیزی از دست نمی‌رود. روشن‌کردن دوباره‌اش همهٔ تاریخ‌های جاافتاده را جبران می‌کند، و ربات هنگام شروع هشدار می‌نویسد تا یک off فراموش‌شده بی‌صدا اجاره را متوقف نکند.

با بیش از یک پیام‌رسان، هر فرایند ربات این حلقه را اجرا می‌کند. قفل ردیف نمی‌گذارد دوتایشان یک تاریخ را دوبار ثبت کنند. پیام به مالک از همان فرایندی فرستاده می‌شود که ثبت کرده، پس فقط در همان پیام‌رسان به دستش می‌رسد.

جابه‌جایی بین پولینگ و وب‌هوک

# در /opt/kasbbook/.env
KASBBOOK_UPDATE_MODE=webhook
KASBBOOK_WEBHOOK_PATH=$(python3 -c 'import secrets; print(secrets.token_urlsafe(24))')
KASBBOOK_API_URL=https://your.host

بعد هر دو یونیت را دوباره راه بینداز. پروسهٔ ربات موقع بالا آمدن وب‌هوک را ثبت می‌کند و از آن به بعد فقط یادآور می‌فرستد؛ API به‌روزرسانی‌ها را سرو می‌کند.

برگشتن هم همان فایل است: KASBBOOK_UPDATE_MODE=polling بگذار و دوباره راه بینداز. خودِ پولر وب‌هوک ثبت‌شده را حذف می‌کند، پس لازم نیست چیزی دستی لغو ثبت شود.

دو چیز را پیش از جابه‌جایی بدان:

  • حالا راه‌اندازی دوبارهٔ kasbbook-api پنجره‌ای است که به‌روزرسانی‌ها در آن گم می‌شوند. با پولینگ سمت سرویس‌دهنده صف می‌کشند و بعداً می‌رسند؛ با وب‌هوک سرویس‌دهنده مدتی تلاش می‌کند و بعد رهایشان می‌کند. update.sh همان یونیت را دوباره راه می‌اندازد.
  • راز در نشانی است، پس nginx نباید لاگش کند. بلوک این میزبان access_log را برای مسیر وب‌هوک خاموش می‌کند؛ بدونش هر به‌روزرسانی راز را روی دیسک می‌نویسد.

لاگ‌ها

journalctl -u kasbbook-bot -f
journalctl -u kasbbook-api --since "1 hour ago" | grep -i error
journalctl -u kasbbook-bot --since today -o cat | grep Traceback

httpx روی WARNING قفل است چون در سطح INFO نشانی کامل را لاگ می‌کند و هر API رباتی توکن را داخل مسیر می‌گذارد. برای اشکال‌زدایی چیزی بالا نبرش؛ یک بار همین‌طور توکنی سر از ژورنال اینجا درآورده.

یکپارچه‌سازی پیوسته

سه کار روی هر پوش، همه روی پایتون ۳٫۱۲:

کار چه اجرا می‌کند
tests pyflakes روی src/ و apps/ و tests، بعد کل مجموعه
postgres تمرین مهاجرت در برابر postgres:16-alpine واقعی
services tests/test_concurrency_services.py: برداشت هم‌زمان پیش‌نویس روی redis:7-alpine واقعی، و اجرای هم‌زمان تکرارشونده‌ها روی پستگرس واقعی

دومی وجود دارد چون ADD COLUMN NOT NULL روی جدول پرشده در SQLite مشکلی ندارد و روی پستگرس شکست می‌خورد — و تنها جایی که این ترکیب رخ می‌دهد محیط عملیاتی است. پیش از نوشته‌شدنش سه استقرارِ بی‌صدا هزینه داشت.