Free

דיבוג Rails בייצור עם Claude — בלי SSH ובלי פריסה מחדש

אבחון + תיקון נתונים Rails ייצור: kamal exec, `?` רוחב מלא, Base64, רצף thread.


400/500 בייצור לא דומה בכלל ללוקאל. בלוקאל אתה מריץ בדיקות שוב, עורך, מריץ שוב. בייצור, המשתמש כבר לחץ על "שלם" ומביט במסך ריק.

שלוש דרכים:

  1. SSH לשרת — עובד, אבל ה-shell בתוך הקונטיינר אולי בלי rails console, השינויים שלך בלי תיעוד, טעות הקלדה אחת ואתה בצרה
  2. פריסה מחדש — דחוף fix, מינימום 10+ דקות, אולי חוסר זמינות קצר, וחלק גדול מהבעיות בכלל לא של קוד (אלא של נתונים או credentials)
  3. הפעלת Rails runner בתוך הקונטיינר הפועל — בלי איתחול, בלי פריסה מחדש, בלי SSH, דיוק של אזמל

המאמר הזה על הדרך השלישית. שני מקרים אמיתיים: ברוחב מלא שנתקע ב-credentials והפיל תשלומי x402 עם 500, וטוויט באמצע thread שה-status: :failed שלו צריך לנסות שוב תוך שמירה על רצף ה-thread. לאורך הדרך, Claude ינסה ללכת ב-4 כיוונים שגויים כברירת מחדל — צריך לתפוס כל אחד.


הכלי: kamal app exec --reuse

Kamal היא כלי הפריסה של Rails מ-37signals. יש לה פקודה:

kamal app exec --reuse 'bin/rails runner "..."'

--reuse פירושה: אל תרים קונטיינר חדש, הפעל בתוך קונטיינר web שכבר רץ. בלי build חדש, בלי docker pull, בלי איתחול, בלי הזרקה מחדש של ENV. הפקודה רצה, הקונטיינר חוזר לטפל בבקשות.

הפלט זורם דרך stdout לטרמינל שלך — בעצם puts בתוך קונסולת Rails ייצור, בלי SSH, tmux או לעזוב את הלפטופ.

סשן טיפוסי:

$ kamal app exec --reuse 'bin/rails runner "puts User.count"'
Launching command with version abc123 from existing container...
  INFO [ok] Finished in 3.8 seconds with exit status 0
App Host: deploy.how2claude.com
12847

הלוך ושוב ב-3-6 שניות. שני סדרי גודל יותר מהר מפריסה מחדש.

מקרה 1: רוחב מלא שגרם ל-500 בייצור

commit eba9ac9.

תסמין: כמה שעות אחרי שחרור תשלומי x402, כל בקשת תשלום חזרה ב-500. הלוגים מלאים ב-Net::HTTPBadResponse מה-HTTP client. בלוקאל הכל עובד מושלם.

אבחון: תן ל-Claude קודם להדפיס את הגדרות x402 בייצור:

kamal app exec --reuse 'bin/rails runner "puts X402.configuration.wallet_address.inspect"'

פלט:

"0xAbC123...def?"

יש מיותר בזנב — סימן שאלה ברוחב מלא (U+FF1F), לא ? בחצי רוחב. ה-IME של מישהו התחלף תוך כדי עריכת config/credentials/production.yml.enc, והתו הזה התגנב פנימה.

ה-config/credentials.yml.enc הלוקאלי (מפוענח עם master.key של dev/test) לא מכיל את זה — ב-Rails 8, production ו-dev הם encrypted credentials נפרדים, התוכן לא משותף.

תיקון: אי אפשר לעשות SSH ולערוך את הקובץ ישירות (מוצפן), ואי אפשר למשוך אותו לוקאלית ולערוך (ה-master.key לא בלפטופ). המהלך הוא לתת ל-Claude לכתוב סקריפט Ruby חד-פעמי ולהזריק אותו דרך EDITOR= אל credentials:edit:

# script/fix_prod_wallet.rb
content = File.read(ARGV[0])
# הסר סימן שאלה רוחב מלא מהזנב
content.gsub!(/(wallet_address: 0x[0-9a-fA-F]+)?\s*$/, '\1')
File.write(ARGV[0], content)
EDITOR="ruby script/fix_prod_wallet.rb" \
  bin/rails credentials:edit --environment production

זרימת credentials:edit: פענוח → כתיבה לקובץ זמני → קריאה ל-$EDITOR → הצפנה מחדש → מחיקת הזמני. החלפת EDITOR בסקריפט Ruby שלנו מבצעת את העריכה אוטומטית, בלי לבהות בטקסט המוצפן לוקאלית.

אחר כך git commit + kamal deploy פעם אחת — הפריסה הזו חובה כי production.yml.enc השתנה. אבל האבחון לא עלה פריסה.

כלל: כשהייצור נשבר, קרא קודם עם kamal app exec --reuse. אל תנחש, אל תפרוס מחדש קודם.

מקרה 2: שחזור XQueue::Tweet שנכשל באמצע thread

תסמין: אחרי שמאמר מתפרסם, טוויטים נכנסים לטבלת x_queue_tweets. אחד מהם — השני בתוך thread של 4 — מסתיים ב-status: :failed (רייט לימיט של X API, ולידציית תוכן, כל סיבה). שחזור דורש שמירה על רצף ה-thread עם הטוויט הראשון.

מציאת הנכשל:

kamal app exec --reuse 'bin/rails runner "
  XQueue::Tweet.where(status: :failed).each do |t|
    puts \"#{t.id}: thread=#{t.thread_id} pos=#{t.thread_position} content=#{t.content.inspect}\"
  end
"'

מתברר id=87, thread_id=15, thread_position=2.

מלכודת 1: escape של shell. תוכן טוויט מכיל הרבה פעמים מירכאות, שורות חדשות, backticks. אם תכתוב:

# מתפוצץ — shell אוכל מירכאות ולוכסנים הפוכים
kamal app exec --reuse 'bin/rails runner "t = XQueue::Tweet.find(87); t.update!(content: \"...\")"'

ריקוד ה-Base64 — קודד לוקאלית, העבר מחרוזת base64, פענח בתוך ה-runner:

# קודד לוקאלית
echo -n 'תוכן טוויט שנכתב מחדש...' | base64
# => 15rXldeb158g15jXldeZ15jXlSDXqdep16DXm9eq15EuLi4=

# העבר
kamal app exec --reuse "bin/rails runner \"
  t = XQueue::Tweet.find(87)
  t.update!(content: Base64.decode64('15rXldeb158g15jXldeZ15jXlSDXqdep16DXm9eq15EuLi4='), status: :scheduled)
  puts t.status
\""

מחרוזות Base64 הן ASCII טהור, בטוחות ל-shell.

מלכודת 2: רצף ה-thread. XQueue::PostTweetJob.perform_later(87) מפרסם טוויט עצמאי — לא משתרשר לטוויט #1 — כי X API צריך reply_to_tweet_id, וה-Job בברירת מחדל לא נושא ערך כזה.

מצא את ה-x_tweet_id של הטוויט הקודם (שליחות מוצלחות ממלאות את השדה הזה):

kamal app exec --reuse "bin/rails runner \"
  t = XQueue::Tweet.find(87)
  prev = XQueue::Tweet.where(thread_id: t.thread_id, thread_position: t.thread_position - 1).first
  puts 'prev x_tweet_id: ' + prev&.x_tweet_id.to_s
\""
# => prev x_tweet_id: 1834567890123456789

הכנס לתור עם יעד ה-reply:

kamal app exec --reuse 'bin/rails runner "
  XQueue::PostTweetJob.perform_later(87, reply_to_tweet_id: \"1834567890123456789\")
  puts \"enqueued\"
"'

ה-polling_interval של ה-worker הוא 0.1 שניות — הוא תופס את ה-job כמעט מיידית. כמה שניות אחר כך, kamal app exec לראות את ה-status עובר מ-scheduled ל-posted וה-x_tweet_id מתמלא — ה-thread רציף.

כלל: פעולות נתונים בייצור חייבות לכבד אילוצי שכבת העסקים, לא רק "הרשומה עודכנה בהצלחה". רצף ה-thread הוא אילוץ עסקי; Rails runner לא בודק אותו עבורך.

4 הכיוונים שבהם Claude סוטה כברירת מחדל (ואיך לכוון מחדש)

בעבודה כזו, הריאקציה הראשונה של Claude לעיתים קרובות שגויה. תפוס כל אחד:

1. רוצה לעשות SSH לשרת

"בוא אעשה SSH ואציץ..."

כוון מחדש: kamal app exec --reuse מנצח את ה-SSH — בתוך הקונטיינר, סביבת Rails טעונה, מתועד (לוגי kamal שומרים רשומה), לא נוגע ב-shell של המארח, בלי דאגות drift של קונטיינר (reuse מבטיח את גרסת הייצור הנוכחית).

2. רוצה לכתוב migration לתקן נתונים

"אכתוב migration שיסיר את ברוחב מלא מ-wallet_address..."

כוון מחדש: שינוי ערך credentials אחד לא צריך migration (בבסיס הנתונים לא נגעו). Rails runner חד-פעמי עושה את זה ב-10 שניות; migration דורש פריסה ונשאר ב-schema לנצח. השתמש ב-migration רק אם התיקון עשוי להתרחש שוב; טעות הקלדה היא דבר חד-פעמי.

3. דוחף תווים מיוחדים למחרוזות shell

"פשוט אקרא ל-update!(content: "...")..."

כוון מחדש: כל תוכן שנוצר על ידי משתמש (טוויטים, תגובות, markdown מוזן) צריך לעבור דרך Base64. פירוק shell של מירכאות, לוכסנים הפוכים, $ ו-backticks הוא שדה מוקשים קלאסי — ייצור הוא מקום גרוע לתרגל בו.

4. perform_later בלי פרמטרים עסקיים

"פשוט אריץ מחדש PostTweetJob.perform_later(87)..."

כוון מחדש: שאל קודם "האם הרשומה הזו קשורה לאחרות?" ל-threads יש יחסי reply_to, ל-batch jobs יש batch_id, ל-jobs עם paging יש cursor — רשימת הארגומנטים של Job היא נשאית היחסים העסקיים האלה. הורד ארגומנט אחד, שובר שרשרת אחת.

רשימת בדיקה

דיבוג + שינוי נתונים Rails בייצור עם Claude — 6 כללים:

  1. קרא לפני שאתה כותב. kamal app exec --reuse 'bin/rails runner "puts X"'. אתר את הבעיה לפני שאתה משנה כל דבר.
  2. kamal app exec --reuse הוא הכלי ברירת המחדל, לא SSH, לא פריסה מחדש. בתוך הקונטיינר, Rails טעון, 3-6 שניות הלוך ושוב.
  3. שינויי credentials דרך EDITOR=ruby-script bin/rails credentials:edit --environment production. סקריפט Ruby עושה את העריכה, בלי להביט בטקסט המוצפן לוקאלית.
  4. escape של shell הוא שדה מוקשים; נתב תוכן מיוחד דרך Base64. echo -n 'X' | base64 לוקאלית, Base64.decode64 ב-runner.
  5. Rails runner לא אוכף אילוצים עסקיים עבורך. reply_to של thread, batch_id, cursor של paging — הכנס לתור עם כולם.
  6. כתוב migration רק אם "זה עשוי לקרות שוב". תיקוני נתונים חד-פעמיים דרך runner, שני סדרי גודל יותר מהר.

דיבוג בייצור זה לא להשתיל את זרימת הפיתוח שלך לייצור — אין לך מרחב איטרציה, אין לך סובלנות לטעויות. מה שאתה באמת משתמש בו זה משטח האינטרוספקציה שייצור כבר מציע (Rails runner + kamal exec + credentials:edit), כל צעד עושה את השינוי הקטן ביותר האפשרי. Claude יכול לכתוב Ruby נכון — אבל לדעת "מה אפשר לעשות ישירות מול מה שצריך לאבחן קודם" זו החלטה שהוא לא יעשה עבורך. זו המשמעת הייצורית שלך.