🔁 ทวนก่อนเริ่ม — บทนี้ใช้คำพวกนี้
- log ของ request — บรรทัด
Started POST …ถึงCompleted 302 Found in …msคือหนึ่งใบสั่งอาหาร · ตัวเลข ms คือลูกค้ายืนรอนานแค่ไหน (Day 2–3) - callback (บท 112) — โค้ดที่ Rails เรียกเองตอนบันทึก · กฎในบทนั้น: callback ควรดูแลแค่แฟ้มของตัวเอง งานที่ต้องคุยกับคนอื่น (อีเมล ระบบอื่น) ให้คิดก่อนว่าควรอยู่ใน callback จริงไหม · และบอกว่าอีเมลแจ้งผู้ติดตาม "ฝากให้ job ส่งทีหลัง"
- goroutine (Go บท 21) —
go f()แยกงานไปทำพร้อมกัน · main จบ = goroutine ที่ค้างตายหมดแบบเงียบ ๆ - worker pool (Go บท 26) — งานกองรวมใน channel
jobsแล้วมีคนงานหลายตัวมาหยิบไปทำ - idempotency (Go บท 36) — ยิงซ้ำกี่ครั้งผลก็เหมือนเดิม · และ
find_or_create_by!ใน seeds (บท 120) ที่รันซ้ำแล้วข้อมูลไม่เบิ้ล
🧰 เวอร์ชันที่ใช้ตรวจ: Ruby 4.0.9 · Rails 8.1.4 · PostgreSQL 18 (สไลด์และ repo ของอาจารย์เขียน Ruby 4.0.5–4.0.6 · Rails 8.1.3 — เรื่องในบทนี้ไม่ต่างกัน)
📍 โค้ดเริ่มจาก: ตอนจบบท 141 (rich text + รูปปก + prompts/04-content.md) ที่ commit แล้ว — git status --short ต้องว่าง ·
บทนี้ทดลองแล้วคืนทิ้งตอนจบ (ภารกิจท้ายบทพาเก็บกวาด) · ของจริงที่อยู่ถาวรเริ่มในบท 143
🖥 เปิดค้างไว้: docker compose up -d (ฐานข้อมูล + MinIO) · bin/dev หนึ่งหน้าต่าง — บทนี้อ่าน log จากหน้าต่างนี้ตลอด · อีกหน้าต่างไว้พิมพ์คำสั่ง · เบราว์เซอร์ที่ localhost:3000 · ยังไม่ต้องเปิดโปรแกรมทำ job แยก (เริ่มต้องเปิดในบท 143)
ผลรันทุกกล่องมาจากเครื่องจริง (สร้างบทความผ่านฟอร์มใน Chrome) · log ตัดบรรทัดที่ไม่เกี่ยว (SQL · บรรทัด ↳ · การโหลดหน้า) เป็น … และตัดคอมเมนต์ /*action=…*/ ท้าย SQL ออก ·
บรรทัด css.1 | Done in … (ตัวสร้าง CSS ของ Tailwind) จะแทรกขึ้นมาเป็นระยะในหน้าต่างของคุณ — ไม่เกี่ยวกับบทนี้ ตัดออกหมด ·
เวลา · เลข ms · Job ID · เลข id ของบทความ ของคุณจะต่าง
เปิดบล็อก E — งานเบื้องหลัง และประโยคข้อ ② ของวัน
บล็อก D (บท 136–141) ทำให้บทความมีเนื้อหาจริง · บล็อก E มีเป้าหมายเดียว: ตั้งเวลาเผยแพร่บทความ แล้วให้ระบบเผยแพร่เองพร้อมส่งอีเมลหาผู้ติดตาม · ก่อนจะทำแบบนั้นได้ ต้องรู้จัก "งานที่ทำทีหลัง โดยไม่มีใครยืนรอ" ก่อน — นั่นคือบทนี้
| บท | เรื่อง |
|---|---|
| 142 (บทนี้) | ทำไมต้องมีงานเบื้องหลัง · job ตัวแรก · ทำไม "ทำพอดีครั้งเดียว" ไม่มีจริง — ยังไม่มีโค้ดถาวร |
| 143 | ออกแบบ job ให้รันซ้ำแล้วไม่พัง · คิวที่เก็บในฐานข้อมูล (Solid Queue) |
| 144 | ลองใหม่เมื่อพัง · งานตามเวลา · อีเมลถึงผู้ติดตาม |
| 145 | Lab — ระบบตั้งเวลาเผยแพร่ที่รันซ้ำแล้วไม่พัง |
ประโยคข้อ ② ของวัน (สไลด์หน้า 32): "exactly-once ไม่มีอยู่จริง ออกแบบให้ทำซ้ำได้แทน" · ภาษาคน: ไม่มีระบบไหนรับประกันได้ว่างานจะถูกทำ พอดีหนึ่งครั้ง · ครึ่งหลังของบทนี้จะพิสูจน์ว่าทำไม
ลองทำให้หน้าค้างเอง — งาน 9 วินาทีใน request เดียว
สไลด์หน้า 16 ยกตัวอย่าง action create ที่บันทึกบทความแล้วทำต่ออีกสามอย่าง: ส่งอีเมลแจ้งผู้ติดตาม (3 วิ) · ส่งเข้าระบบค้นหา (2 วิ) · ย่อรูปปก (4 วิ) ·
แอปเรายังไม่มีทั้งสามอย่าง เลยจำลองด้วย sleep (สั่งให้โปรแกรมหยุดรอ N วินาที) — ความช้าเท่ากัน แค่ไม่ได้ทำงานจริง
ขั้น 1 · เปิด app/controllers/posts_controller.rb หา action create แล้วเติมสามบรรทัด sleep ใต้ if @post.save:
def create
@post = Post.new(post_params)
respond_to do |format|
if @post.save
sleep 3 # จำลอง: ส่งอีเมลแจ้งผู้ติดตาม
sleep 2 # จำลอง: ส่งเข้าระบบค้นหา
sleep 4 # จำลอง: ย่อรูปปก
format.html { redirect_to @post, notice: "สร้างบทความแล้ว" }
# … บรรทัดที่เหลือเหมือนเดิม
- บรรทัด
sleep 3— หยุดรอ 3 วินาที แทน "ส่งอีเมล" · อีกสองบรรทัดเหมือนกัน รวม 9 วินาที - ทั้งสามบรรทัดอยู่หลัง
@post.save— บทความถูกบันทึกลงตู้เรียบร้อยก่อนจะเริ่มรอ - และอยู่ก่อน
redirect_to— ผู้ใช้จะยังไม่ได้หน้าถัดไปจนกว่าจะรอครบ
ขั้น 2 · บันทึกไฟล์ (bin/dev โหลดโค้ดใหม่ให้เอง) · เปิด localhost:3000/posts/new กรอกหัวข้อ ทดลองงานช้า พิมพ์เนื้อหาอะไรก็ได้ เลือกหมวด (ไม่ต้องใส่รูป) แล้วกด บันทึก
หน้าจอจะนิ่งไปนานจนน่าสงสัยว่าเว็บพัง · ในเครื่องที่ตรวจ Chrome ใช้เวลา 9,181 ms นับจากกดปุ่มถึงเห็นหน้าบทความ ·
log ในหน้าต่าง bin/dev (10:20:50 web.1 | หน้าบรรทัด = เวลา + มาจากตัวเว็บ):
10:20:50 web.1 | Started POST "/posts" for ::1 at 2026-11-10 10:20:50 +0700
10:20:50 web.1 | Processing by PostsController#create as TURBO_STREAM
10:20:50 web.1 | Parameters: {"authenticity_token" => "[FILTERED]", "post" => {"title" => "ทดลองงานช้า", "body" => "<div>ทดลองงานหลังร้าน</div>", "category_id" => "1"}, "commit" => "บันทึก"}
10:20:50 web.1 | TRANSACTION (1.7ms) BEGIN
10:20:50 web.1 | …
10:20:50 web.1 | Post Create (1.6ms) INSERT INTO "posts" ("title", "description", "category_id", "created_at", "updated_at", "plain_text") VALUES ('ทดลองงานช้า', …) RETURNING "id"
10:20:50 web.1 | …
10:20:50 web.1 | TRANSACTION (0.9ms) COMMIT
10:20:59 web.1 | Redirected to http://localhost:3000/posts/31
10:20:59 web.1 | ↳ app/controllers/posts_controller.rb:35:in 'block (2 levels) in PostsController#create'
10:20:59 web.1 | Completed 302 Found in 9049ms (ActiveRecord: 9.4ms (5 queries, 0 cached) | GC: 0.0ms)
for ::1= ใบสั่งมาจากเครื่องตัวเอง (::1คือ localhost) ·Parameters= ของที่กรอกในฟอร์ม · ทั้งคู่เห็นมาตั้งแต่ Day 2–3COMMITเกิดตอน 10:20:50 — ตู้ยืนยันแล้วว่าบทความเลข 31 อยู่ในตู้ งานของผู้ใช้เสร็จตั้งแต่วินาทีแรกRedirectedเกิดตอน 10:20:59 — 9 วินาทีหลังจากนั้น ผู้ใช้เพิ่งได้หน้าถัดไป · บรรทัด↳ …posts_controller.rb:35ใต้มัน = คำสั่ง redirect มาจากบรรทัด 35 ของ controllerCompleted 302 Found in 9049msคือเวลาที่ลูกค้ายืนรอทั้งหมด ·ActiveRecord: 9.4msคือเวลาที่ใช้กับตู้จริง ๆ — ไม่ถึง 0.2% ของเวลารอ
นี่คือประโยคใต้โค้ดของสไลด์หน้า 16: ผู้ใช้หน้าค้าง 9 วินาที ทั้งที่งานของเขาเสร็จไปตั้งแต่วินาทีแรก · และถ้าระบบอีเมลล่มตอนนั้น ผู้ใช้จะได้หน้า error ทั้งที่บทความบันทึกไปแล้ว — คิดว่าไม่สำเร็จ แล้วกดบันทึกซ้ำ ได้บทความสองชิ้น
ยังไม่ต้องลบ sleep · หัวข้อถัดไปจะย้ายมันไปอยู่ที่ใหม่ · ถ้าคุณกดบันทึกซ้ำเพราะคิดว่าเว็บค้าง จะได้บทความ "ทดลองงานช้า" สองชิ้น — ปล่อยไว้ ภารกิจท้ายบทพาลบ
เส้นแบ่ง — "ถ้างานนี้ยังไม่เสร็จ ผู้ใช้เห็นหน้าถัดไปได้ไหม"
สไลด์หน้า 16 ให้คำถามข้อเดียวไว้ตัดสินทุกงานที่อยู่ใน action:
- ได้ → ไม่ต้องให้ผู้ใช้รอ · ส่งไปทำเบื้องหลัง
- ไม่ได้ → ต้องทำให้เสร็จใน request (ในใบสั่งนี้) ก่อนตอบ
| งาน | ยังไม่เสร็จ ผู้ใช้เห็นหน้าถัดไปได้ไหม | ไปไหน |
|---|---|---|
| บันทึกบทความลงตู้ | ไม่ได้ — หน้าถัดไปคือหน้าบทความ ยังไม่มีบทความก็ไม่มีอะไรให้ดู | ใน request |
| ตรวจว่าหัวข้อไม่ว่าง (validation) | ไม่ได้ — ต้องรู้ก่อนว่าจะโชว์หน้าบทความหรือโชว์ error | ใน request |
| ส่งอีเมลแจ้งผู้ติดตาม | ได้ — ช้าไป 5 วินาทีก็ไม่มีใครรู้ | เบื้องหลัง |
| ส่งเข้าระบบค้นหา | ได้ — ค้นเจอช้าไปไม่กี่วินาทีไม่มีใครเดือดร้อน | เบื้องหลัง |
| ย่อรูปปกหลายขนาด | ได้ — หน้าบทความโชว์รูปเดิมไปก่อนได้ | เบื้องหลัง |
ทวนบท 112: ตอนเรียน callback เราได้กฎว่า "callback ควรดูแลแค่แฟ้มของตัวเอง" · และบอกว่าอีเมลแจ้งผู้ติดตาม "ฝากให้ job ส่งทีหลัง" แต่ยังไม่ได้เล่าว่า job คืออะไร (บท 112 เขียนว่า "เรื่องของ Day 3" — ของจริงมาถึงตอนนี้) · job คือปลายทางของคำว่า "ฝาก" นั่นเอง
สไลด์ให้เหตุผลข้อที่สองด้วย: งานที่อยู่ใน job พังแล้วลองใหม่ได้ · งานที่อยู่ใน request พังแล้วหายไปเลย · ประโยคนี้จริงเมื่อคิวเก็บงานไว้ถาวรและตั้งให้ลองใหม่ — สองอย่างที่บท 143–144 จะตั้ง · ในบทนี้คุณจะเห็นว่ายังไม่จริง
background job คืออะไร — ใบงานหลังร้าน
background job คือ "งานที่จดไว้ให้ทำทีหลัง" · controller ไม่ทำเอง แค่เขียนใบงานแขวนไว้ แล้วตอบผู้ใช้ทันที · จะมีโปรแกรมอีกตัวมาหยิบใบงานไปทำตอนว่าง โดยไม่มีผู้ใช้คนไหนยืนรอ
ที่แขวนใบงานเรียกว่า คิว (queue = แถวรอ ใครมาก่อนได้ทำก่อน) · โปรแกรมที่มาหยิบใบงานไปทำเรียกว่า worker (คนงาน)
ลูกค้าสั่งข้าวผัด (request) · หัวหน้าเชฟผัดเสร็จ ยกจานให้ลูกค้า (response) — จบสำหรับลูกค้า · แต่ร้านยังมีงานที่ต้องทำต่อ: ล้างกระทะ · โทรสั่งวัตถุดิบ · ส่งข้าวกล่องไปบ้านลูกค้าอีกคน
เชฟไม่ทำเองตอนนั้น แค่เขียนใบงานหลังร้านแขวนไว้บนกระดาน (คิว) แล้วกลับไปรับออเดอร์ต่อ · พนักงานหลังร้าน (worker) เดินมาหยิบใบงานไปทำทีละใบ
ที่ไม่เหมือนกัน: ร้านจริงถ้าไม่มีพนักงานหลังร้าน ใบงานก็แขวนอยู่เฉย ๆ ไม่มีใครทำ — กับ Rails ก็เป็นแบบนั้นจริงในบท 143 (ต้องเปิด worker แยก) · แต่ในบทนี้ Rails ยังใช้ทางลัดที่ให้ "หัวหน้าเชฟพกผู้ช่วยติดตัวมาเอง" (หัวข้อหลังจากนี้) · และพนักงานในร้านจริงจำได้ว่าทำอะไรไปแล้ว ส่วน worker จำอะไรไม่ได้นอกจากที่เขียนบนใบงาน
เทียบกับ Go (บท 26 worker pool): channel jobs ที่กองงานไว้ = คิว · goroutine ที่วนรับงานจาก jobs = worker ·
ความต่างคือ worker pool ของ Go อยู่ในโปรแกรมเดียวกับคนส่งงาน · คิวของจริงที่ใช้ใน production (บท 143) อยู่นอกโปรแกรมเว็บ — ต่างกันตรงนี้แหละที่ทำให้งานไม่หาย
- background job
- งานที่แยกออกจาก request ไปทำทีหลัง ผู้ใช้ไม่ต้องรอ · ถาม senior ได้ว่า "งานนี้ควรอยู่ใน request หรือทำเป็น background job ครับ"
- queue (คิว)
- ที่เก็บงานที่รอทำ เรียงตามลำดับ · คำที่ได้ยินบ่อย: "job ค้างในคิว" = ใบงานแขวนอยู่ ยังไม่มีใครหยิบ
- worker
- โปรแกรมที่หยิบงานจากคิวไปทำ · "worker ล่ม" = ไม่มีใครทำงาน ใบงานกองสูงขึ้นเรื่อย ๆ
- enqueue
- เอางานเข้าคิว (แขวนใบงาน) · พิมพ์บอก AI ได้ว่า "move the email sending out of the request and enqueue a background job instead"
job ตัวแรก — ย้าย 9 วินาทีออกไปหลังร้าน
ขั้น 1 · สร้างไฟล์ job ด้วยตัวสร้างไฟล์ของ Rails (รันในโฟลเดอร์โปรเจกต์):
$ bin/rails generate job slow_work
invoke test_unit
create test/jobs/slow_work_job_test.rb
create app/jobs/slow_work_job.rb
ได้สองไฟล์ · ไฟล์ test ปล่อยไว้ก่อน (เรื่อง test อยู่ใน Part 2 ของสไลด์) · ชื่อ slow_work กลายเป็นคลาส SlowWorkJob (Rails เติมคำว่า Job ท้ายชื่อให้เอง)
ขั้น 2 · เขียนงาน — เปิด app/jobs/slow_work_job.rb แก้ทั้งไฟล์เป็น:
class SlowWorkJob < ApplicationJob
queue_as :default
def perform(post_id)
post = Post.find(post_id)
sleep 3 # จำลอง: ส่งอีเมลแจ้งผู้ติดตาม
sleep 2 # จำลอง: ส่งเข้าระบบค้นหา
sleep 4 # จำลอง: ย่อรูปปก
Rails.logger.info "งานหลังร้านของบทความ #{post.id} เสร็จแล้ว"
end
end
< ApplicationJob— สืบทอดจากแม่แบบของ job ทุกตัวในแอป (เหมือน model สืบจากApplicationRecord) · แม่แบบนี้มาจาก Active Job = ชุดเครื่องมือทำ job ที่มากับ Rails (บท 143 เล่าต่อ)queue_as :default— แขวนใบงานไว้บนกระดานชื่อ default · แอปเล็ก ๆ ใช้กระดานเดียวพอdef perform(post_id)— สิ่งที่ worker ทำเมื่อหยิบใบงานนี้ไป · ชื่อperformบังคับPost.find(post_id)— ใบงานจดแค่เลข id ของบทความ worker ต้องไปหยิบแฟ้มจากตู้เอง (ทำไมจดแค่เลข — บท 143)sleepสามบรรทัด — ย้ายมาจาก controller ตรง ๆRails.logger.info "…"— เขียนหนึ่งบรรทัดลง log ตอนทำครบ · เราจะใช้บรรทัดนี้เป็นหลักฐานว่างานเสร็จจริง
ขั้น 3 · ให้ controller แขวนใบงานแทนทำเอง — กลับไปที่ create ลบ sleep สามบรรทัด ใส่บรรทัดเดียวแทน:
ก่อน — ทำเองใน request
if @post.save
sleep 3 # จำลอง: ส่งอีเมล…
sleep 2 # จำลอง: ส่งเข้า…
sleep 4 # จำลอง: ย่อรูปปก
format.html { … }
หลัง — แขวนใบงาน
if @post.save
SlowWorkJob.perform_later(@post.id)
format.html { … }
perform_later— "ทำทีหลัง" · เขียนใบงาน (ชื่อ job + เลข id) แขวนเข้าคิว แล้วกลับมาทันที ไม่รอให้performทำ(@post.id)— ของที่ส่งไปจะกลายเป็นpost_idในperform- บรรทัดนี้อยู่หลัง
saveสำเร็จเท่านั้น — บันทึกไม่ผ่านก็ไม่มีใบงาน (ไม่มีอีเมลเรื่องบทความที่ไม่มีอยู่จริง)
บันทึกไฟล์ทั้งสอง แล้วสร้างบทความใหม่หัวข้อ ทดลองใบงาน ผ่านฟอร์มเหมือนเดิม · ดู log ในหน้าต่าง bin/dev
คิดว่า ① บรรทัด Completed 302 Found in …ms จะได้ประมาณเท่าไร ② บรรทัด "งานหลังร้านของบทความ … เสร็จแล้ว" จะโผล่ก่อนหรือหลังบรรทัด Completed และห่างกันกี่วินาที?
ผลจริง (Chrome ใช้เวลา 108 ms นับจากกดปุ่มถึงเห็นหน้าบทความ · Job ID ตัดให้สั้นเป็น 16307e4e-…):
10:21:06 web.1 | Started POST "/posts" for ::1 at 2026-11-10 10:21:06 +0700
10:21:06 web.1 | Processing by PostsController#create as TURBO_STREAM
10:21:06 web.1 | …
10:21:06 web.1 | TRANSACTION (1.2ms) COMMIT
10:21:06 web.1 | [ActiveJob] Enqueued SlowWorkJob (Job ID: 16307e4e-…) to Async(default) with arguments: 32
10:21:06 web.1 | [ActiveJob] ↳ app/controllers/posts_controller.rb:32:in 'block in PostsController#create'
10:21:06 web.1 | Redirected to http://localhost:3000/posts/32
10:21:06 web.1 | Completed 302 Found in 28ms (ActiveRecord: 6.3ms (5 queries, 0 cached) | GC: 0.6ms)
10:21:06 web.1 | [ActiveJob] [SlowWorkJob] [16307e4e-…] Performing SlowWorkJob (Job ID: 16307e4e-…) from Async(default) enqueued at 2026-11-10T03:21:06.636126000Z with arguments: 32
10:21:06 web.1 | Started GET "/posts/32" for ::1 at 2026-11-10 10:21:06 +0700
10:21:06 web.1 | … (หน้าบทความ 32 วาดเสร็จ)
10:21:06 web.1 | Completed 200 OK in 28ms (Views: 15.2ms | ActiveRecord: 4.5ms (6 queries, 0 cached) | GC: 11.9ms)
10:21:06 web.1 | …
10:21:15 web.1 | [ActiveJob] [SlowWorkJob] [16307e4e-…] งานหลังร้านของบทความ 32 เสร็จแล้ว
10:21:15 web.1 | [ActiveJob] [SlowWorkJob] [16307e4e-…] Performed SlowWorkJob (Job ID: 16307e4e-…) from Async(default) in 9031.67ms
- ①
Completed 302 Found in 28ms— จาก 9,049 ms เหลือ 28 ms · ผู้ใช้ได้หน้าบทความทันที Enqueued SlowWorkJob … with arguments: 32— แขวนใบงานแล้ว (บทความเลข 32) · บรรทัด↳ …posts_controller.rb:32บอกว่าแขวนจากบรรทัด 32 ของ controllerPerforming SlowWorkJob— มีคนหยิบใบงานไปทำแล้ว · ทำพร้อม ๆ กับที่เบราว์เซอร์ขอหน้าบทความ (Started GET "/posts/32")- ② "เสร็จแล้ว" โผล่ทีหลัง ตอน 10:21:15 — 9 วินาทีหลังผู้ใช้ได้หน้าไปแล้ว ·
Performed … in 9031.67ms= งานใช้เวลาเท่าเดิม แค่ไม่มีใครต้องยืนรอ Async(default)— ชื่อกระดานdefaultจากqueue_as· ส่วนคำว่าAsyncคือหัวข้อถัดไป
งานไม่ได้เร็วขึ้นเลย — แค่ย้ายไปทำตอนที่ไม่มีใครรอ
ตอนนี้ใบงานแขวนอยู่ที่ไหน — Async คือคิวในหน่วยความจำ
เราไม่ได้ตั้งค่าคิวเลย แต่ job ก็ทำงาน · ถามแอปได้ว่าตอนนี้ใช้คิวแบบไหน (หน้าต่างที่สอง):
$ bin/rails runner 'p ActiveJob::Base.queue_adapter_name'
"async"
bin/rails runner '…' = รันโค้ด Ruby หนึ่งบรรทัดในแอปแล้วจบ (ไม่ต้องเปิด console) · p = พิมพ์ค่าออกมา · ActiveJob::Base = แม่แบบตัวบนสุดของ job ทุกตัว (:: อ่านว่า "ที่อยู่ใน") · ได้ "async" ·
adapter คือ "ตัวเสียบ" ที่บอกว่าใบงานไปเก็บที่ไหน · development ถ้าไม่ตั้งอะไร ได้ async ·
ส่วน config/environments/production.rb (ค่าตั้งของ production = เครื่องจริงที่ผู้ใช้ใช้) ของเรามีบรรทัดนี้อยู่แล้ว:
# Replace the default in-process and non-durable queuing backend for Active Job.
config.active_job.queue_adapter = :solid_queue
คอมเมนต์บรรทัดบนบอกนิสัยของ async ไว้สองคำ: in-process (อยู่ใน process เดียวกับเว็บ — ตัวโปรแกรมที่กำลังรันอยู่ตัวเดียวกัน) และ
non-durable (ไม่ทนทาน — ไม่ได้เขียนเก็บลงที่ไหน) · เอกสาร Rails เขียนตรง ๆ ว่า ถ้า process ดับหรือเครื่องรีเซ็ต งานที่ค้างอยู่หายหมด (ดูลิงก์ท้ายบท)
async เหมือนหัวหน้าเชฟจดใบงานลงกระดาษโน้ตในกระเป๋าเสื้อ แล้วพาผู้ช่วยติดตัวมาเองยืนทำอยู่ข้าง ๆ ในครัวเดียวกัน ·
สะดวก ไม่ต้องติดกระดาน ไม่ต้องจ้างพนักงานหลังร้าน
แต่ถ้าปิดครัว (หยุด server) เชฟเดินออก — โน้ตกับผู้ช่วยหายไปพร้อมกัน ใบงานที่ยังไม่เสร็จไม่มีใครรู้ว่าเคยมี · บท 143 จะเปลี่ยนเป็น Solid Queue = ติดกระดานไว้ในตู้แฟ้ม (ตารางใน PostgreSQL) ปิดครัวแล้วกระดานยังอยู่
ที่ไม่เหมือนกัน: ผู้ช่วยของ async ไม่ได้มีคนเดียว — เป็นกลุ่มคนงานหลายคนในโปรแกรมเดียวกัน (ทำหลายใบงานพร้อมกันได้) ·
และ "ปิดครัว" ในโลกจริงไม่ได้เกิดแค่ตอนคุณกด Ctrl+C — ทุกครั้งที่ deploy (เอาเวอร์ชันใหม่ขึ้นเครื่องจริง) server ถูกปิดแล้วเปิดใหม่ นั่นคือเหตุที่ production ห้ามใช้ async
เทียบกับ Go (บท 21): async ก็คือ go f() ดี ๆ นี่เอง · ถ้าใน handler ของ Gin เราเขียน (ตัดมาสองบรรทัด ไม่ใช่ไฟล์เต็ม):
go sendEmail(post.ID) // แยกไปส่งอีเมล ไม่รอ
c.JSON(201, post) // ตอบผู้ใช้ทันที
ผู้ใช้ได้คำตอบเร็วเหมือนกัน · แต่กฎของบท 21 ยังอยู่: โปรแกรมจบ = goroutine ที่ค้างตายเงียบ ๆ · และถ้า sendEmail พัง ไม่มีใครลองใหม่ให้ ·
go f() แก้ปัญหา "ผู้ใช้ต้องรอ" ได้ แต่ไม่ได้แก้ "งานหาย" — สิ่งที่คิวจริงต้องมีคือที่เก็บใบงานที่อยู่นอกโปรแกรม
- perform_later
- สั่งให้ job ไปเข้าคิว ทำทีหลัง แล้วกลับมาทันที · คู่กับ
performที่เป็นตัวงานจริง · log ฝั่งคนสั่งขึ้นEnqueued· ฝั่งคนทำขึ้นPerforming/Performed - queue adapter
- ตัวเสียบที่กำหนดว่าคิวจริงอยู่ที่ไหน (ในหน่วยความจำ · ในฐานข้อมูล · ใน Redis) · โค้ด job ไม่ต้องแก้เมื่อเปลี่ยน adapter · ถาม senior ได้ว่า "production เราใช้ queue adapter ตัวไหนครับ"
- async adapter (in-process)
- adapter ค่าเริ่มต้นของ development · ทำ job ด้วยคนงานในโปรแกรมเว็บเอง · restart แล้วงานค้างหาย · พิมพ์บอก AI ได้ว่า "don't rely on the async adapter in production, jobs are lost on restart"
- durable
- "ทนทาน" = เก็บงานไว้ในที่ที่รอดจากการปิดเปิดโปรแกรม (เช่น ตารางในฐานข้อมูล) · คิวที่ durable คือสิ่งที่บท 143 ตั้ง
ถึงตรงนี้คิดว่าแก้ได้แล้ว: แค่เปลี่ยนไปใช้คิวที่ durable งานก็ไม่หาย · ไม่หาย — แต่ได้ปัญหาใหม่มาแทน · ครึ่งหลังของบทว่าด้วยเรื่องนี้
exactly-once ไม่มีจริง — เลือกได้แค่ "อาจหาย" หรือ "อาจซ้ำ"
ใบงานหนึ่งใบต้องผ่านสองฝั่ง: คิว (ที่แขวนใบงาน) กับ worker (คนทำ) · สองฝั่งนี้คุยกันผ่านเครือข่ายหรือฐานข้อมูล ซึ่งขาดได้ และ worker ตายได้ทุกเมื่อ · คำถามคือ: คิวจะขีดฆ่าใบงาน (ถือว่าจบ) ตอนไหน — ก่อน worker ทำ หรือหลัง worker ทำ · มีแค่สองทาง (สไลด์หน้า 17):
| at-most-once "ไม่เกิน 1 ครั้ง" | at-least-once "อย่างน้อย 1 ครั้ง" | exactly-once "พอดี 1 ครั้ง" | |
|---|---|---|---|
| ลำดับ | ขีดฆ่าใบงานออกจากคิวก่อน แล้วค่อยทำ | ทำให้เสร็จก่อน แล้วค่อยบอกคิวว่าเสร็จ | สองฝั่งต้องรู้ตรงกันเป๊ะทุกครั้ง |
| worker ตายกลางทาง | ใบงานถูกฆ่าไปแล้ว → งานหาย | คิวยังไม่ได้ยินว่าเสร็จ → แจกใหม่ → งานซ้ำ | ทำไม่ได้ เมื่อสายขาดได้หรือ worker ตายได้ |
| เหมาะกับ | งานที่หายได้ เช่น นับคลิก | ค่าเริ่มต้นของคิวเกือบทุกตัว (สไลด์อ้าง) | ไม่มีอยู่จริง |
แล้วที่คนชอบพูดว่า "ระบบเรา exactly-once" คืออะไร · สไลด์ตอบว่า at-least-once + งานที่ทำซ้ำแล้วผลเหมือนเดิม — คิวส่งซ้ำได้ แต่ทำซ้ำแล้วไม่มีอะไรเสีย ผลรวมเลยดูเหมือนทำครั้งเดียว
ใบงาน "ส่งข้าวกล่องไปบ้านคุณสมชาย" แขวนบนกระดาน · พนักงานส่งของหยิบไป ส่งถึงมือแล้ว เดินกลับมาจะติ๊กกระดานว่าเสร็จ — แต่เป็นลมล้มไปก่อนติ๊ก · พนักงานคนใหม่มาเห็นใบงานยังไม่ติ๊ก ก็หยิบไปส่งอีกรอบ · คุณสมชายได้ข้าวสองกล่อง = at-least-once · ถ้าร้านเปลี่ยนกฎเป็น "ติ๊กก่อนออกจากร้าน" แล้วพนักงานเป็นลมกลางทาง — กระดานบอกว่าเสร็จ แต่คุณสมชายไม่ได้ข้าวเลย = at-most-once
ที่ไม่เหมือนกัน: ในร้านจริงคุณสมชายโทรมาบอกได้ว่า "ได้แล้ว อย่าส่งอีก" · ในระบบจริงไม่มีใครโทรมาบอก — อีเมลที่ส่งออกไปแล้วเรียกคืนไม่ได้ · และคนใหม่ไม่ได้รู้ว่าคนเก่าเป็นลม เขาแค่เห็นว่านานเกินกำหนดแล้วยังไม่ติ๊ก (คิวรอจนหมดเวลา)
- at-most-once
- รับประกันว่าไม่ซ้ำ แต่อาจหาย · ใช้กับงานที่หายได้ไม่เป็นไร
- at-least-once
- รับประกันว่าไม่หาย แต่อาจซ้ำ · ถาม senior ได้ว่า "คิวเราเป็น at-least-once ใช่ไหมครับ job ต้อง idempotent ทุกตัวหรือเปล่า"
- exactly-once (delivery)
- "ส่งพอดีครั้งเดียว" — เมื่อเครือข่ายขาดได้ ทำไม่ได้จริง · ถ้าเห็นเครื่องมือไหนโฆษณาคำนี้ ให้ถามว่า "หมายถึง at-least-once บวก dedup (กรองของซ้ำ) หรือ idempotency ใช่ไหม"
- ack (acknowledge)
- สัญญาณที่ worker บอกคิวว่า "ทำเสร็จแล้ว ขีดฆ่าได้" = การติ๊กกระดาน · คำถามทั้งหัวข้อนี้คือ "ack ก่อนทำ หรือหลังทำ"
worker ตายตอนไหน งานถึงซ้ำ — ช่องว่างที่ปิดไม่ได้
ไล่ตามเวลาแบบสไลด์หน้า 18 · คิวแบบ at-least-once (ทำก่อน แล้วค่อยบอกคิว) · ใบงานคือ "ส่งอีเมลแจ้งผู้ติดตาม":
- ช่องว่างระหว่าง t2 กับ t3 ปิดไม่ได้ — "ส่งอีเมล" กับ "บอกคิว" เป็นสองการกระทำ ต่อให้เขียนโค้ดเร็วแค่ไหน ระหว่างกลางก็กว้างกว่าศูนย์เสมอ · worker ตายพอดีตรงนั้น = ซ้ำ
- t4 ไม่ใช่เรื่องไกลตัว — deploy ทุกครั้งคือการปิด worker เก่าแล้วเปิดตัวใหม่ · ระบบที่ deploy วันละหลายรอบเจอจังหวะนี้แน่นอนสักวัน
สลับลำดับก็ไม่ช่วย: ถ้าบอกคิวว่าเสร็จก่อน (t3 ขึ้นมาก่อน t2) แล้ว worker ตายตรงกลาง — คิวคิดว่าเสร็จ แต่อีเมลไม่เคยออก · เราแค่ย้ายจากปัญหา "ซ้ำ" ไปเป็นปัญหา "หาย" · เลือกได้แค่ว่าจะเจ็บทางไหน
คำถามที่ถูก — "ถ้าซ้ำแล้ว จะไม่เป็นไรได้ยังไง"
งานส่วนใหญ่หายไม่ได้ (อีเมลยืนยัน · เผยแพร่บทความตามเวลา) → เลือก at-least-once ซึ่งแปลว่าต้องยอมรับว่างานจะถูกทำซ้ำ · คำถามจึงเปลี่ยนจาก "จะกันไม่ให้ซ้ำยังไง" (ทำไม่ได้) เป็น "ถ้าซ้ำแล้วจะไม่เป็นไรได้ยังไง"
ซ้ำแล้วพัง
"บวกยอดวิวของบทความอีก 100" · รันครั้งเดียว +100 · รันซ้ำ +200 — ผลเปลี่ยนทุกครั้งที่ทำ
ซ้ำได้ไม่เป็นไร
"ตั้งสถานะบทความเป็นเผยแพร่แล้ว" · รันครั้งเดียวหรือสิบครั้ง ผลเหมือนเดิม — เหมือนพลิกป้ายหน้าร้านเป็น "เปิด" ซ้ำ ป้ายก็ยังเป็น "เปิด"
ฝั่งขวาคือ idempotent — คำเดียวกับที่เจอใน Go บท 36 (PUT ยิงซ้ำได้ POST ยิงซ้ำแล้วได้ของเบิ้ล) และ find_or_create_by! ใน seeds ·
แต่ "ส่งอีเมล" ไม่มีทางทำให้เป็นแบบฝั่งขวาโดยธรรมชาติ — ส่งสองครั้งก็สองฉบับ · 3 ท่าที่ใช้จริงในการทำให้ job แบบนี้ซ้ำได้ คือเรื่องของบท 143
🎯 ภารกิจ — ปิดครัวกลางงาน แล้วดูว่าใบงานไปไหน (~10 นาที)
พิสูจน์ด้วยตาว่า async เป็น "งานที่หายได้" · ต้องทำหัวข้อ "job ตัวแรก" ครบแล้ว (controller มี perform_later · job มี sleep รวม 9 วินาที)
- สร้างบทความใหม่หัวข้อ
ทดลองกด Ctrl+Cผ่านฟอร์ม - ทันทีที่เห็นหน้าบทความ (ภายใน 5 วินาที) สลับไปหน้าต่าง
bin/devแล้วกด Ctrl+C · อ่านบรรทัดท้าย ๆ ให้ครบ - เปิดใหม่ด้วย
bin/dev· รอ 20 วินาที ไม่ต้องทำอะไร · ดูว่ามีบรรทัด[ActiveJob]ขึ้นไหม - เปิด
localhost:3000/postsดูว่าบทความทดลองกด Ctrl+Cอยู่ไหม - เก็บกวาด (หน้าต่างที่สอง):
$ git restore app/controllers/posts_controller.rb $ rm app/jobs/slow_work_job.rb test/jobs/slow_work_job_test.rb $ git status --shortgit status --shortต้องไม่แสดงอะไรเลย · แล้วลบบทความทดลองทุกชิ้น (หัวข้อขึ้นต้นด้วย "ทดลอง") ด้วยปุ่ม ลบบทความ ในหน้าบทความ
ก่อนกดเฉลย: ข้อ 2 จะเห็นบรรทัด "งานหลังร้าน… เสร็จแล้ว" ไหม · จะเห็นคำว่า Performed ไหม · ข้อ 3 เปิดใหม่แล้วงานจะกลับมาทำต่อไหม · ข้อ 4 บทความอยู่ไหม?
ข้อ 2 — ผลจริง (กด Ctrl+C หลังงานเริ่ม 2 วินาที · Job ID ตัดสั้น):
10:22:41 web.1 | [ActiveJob] Enqueued SlowWorkJob (Job ID: 7ffe7c1f-…) to Async(default) with arguments: 34
10:22:41 web.1 | …
10:22:41 web.1 | Completed 302 Found in 34ms (ActiveRecord: 7.4ms (5 queries, 0 cached) | GC: 0.0ms)
10:22:41 web.1 | [ActiveJob] [SlowWorkJob] [7ffe7c1f-…] Performing SlowWorkJob (Job ID: 7ffe7c1f-…) from Async(default) enqueued at 2026-11-10T03:22:41.978400000Z with arguments: 34
10:22:41 web.1 | … (หน้าบทความ 34 วาดเสร็จ)
10:22:44 system | SIGINT received, starting shutdown
10:22:44 web.1 | - Gracefully stopping, waiting for requests to finish
10:22:44 web.1 | === puma shutdown: 2026-11-10 10:22:44 +0700 ===
10:22:44 web.1 | - Goodbye!
10:22:44 web.1 | Exiting
10:22:44 web.1 | [ActiveJob] [SlowWorkJob] [7ffe7c1f-…] Performed SlowWorkJob (Job ID: 7ffe7c1f-…) from Async(default) in 2158.22ms
10:22:44 system | sending SIGTERM to all processes
10:22:44 css.1 | exited with code 0
10:22:44 web.1 | exited with code 0
- ไม่มีบรรทัด "งานหลังร้านของบทความ 34 เสร็จแล้ว" — งานโดนตัดกลางคัน
- แต่มีคำว่า
Performed … in 2158.22ms— ทำไปได้แค่ 2 วินาที (ยังอยู่ในsleep 3แรก) แล้ว Rails เขียนบรรทัดปิดงานให้ตอนโปรแกรมปิด · ลองแล้ว 3 ครั้ง ได้แบบนี้ทุกครั้ง · อย่าเชื่อคำว่า Performed อย่างเดียว ดูหลักฐานว่างานทำครบจริง SIGINT received— SIGINT คือสัญญาณ "ขอให้หยุด" ที่ Ctrl+C ส่งไป · บรรทัดที่ขึ้นต้นด้วยsystemคือตัวbin/devเอง (foreman ที่เปิดทุกโปรแกรมพร้อมกัน — บท 120)sending SIGTERM to all processes— SIGTERM คือสัญญาณ "ปิดตัวได้แล้ว" อีกแบบหนึ่ง ·bin/devส่งให้ทุกโปรแกรมที่มันเปิดไว้ ·css.1คือตัวคอยสร้างไฟล์ Tailwind ใหม่ ปิดไปพร้อมกันGracefully stopping, waiting for requests to finish— Puma (ตัวรับ request) รอให้ request ที่ค้างเสร็จ · ไม่ได้รอ job
ข้อ 3 — ไม่กลับมา · ผลจริง: เปิดใหม่ถึงบรรทัด Use Ctrl-C to stop แล้วรอ 25 วินาที ไม่มีบรรทัด [ActiveJob] สักบรรทัด · ใบงานอยู่ในหน่วยความจำของโปรแกรมที่ปิดไปแล้ว ไม่มีที่ไหนจำไว้ว่าเคยมี
ข้อ 4 — บทความอยู่ · บันทึกเสร็จ (COMMIT) ก่อนแขวนใบงาน · ผลคือ บทความมี แต่งานหลังร้านของมันหายไปเงียบ ๆ — ถ้าเป็นอีเมลจริง ผู้ติดตามจะไม่ได้อีเมลและไม่มีใครรู้ ·
นี่คือ "งานหาย" แบบเดียวกับ go f() ที่ main จบก่อน · บท 143 แก้ด้วยคิวที่เก็บใบงานในฐานข้อมูล
ถ้าเจออาการนี้ แปลว่า…
- เห็น "งานหลังร้านของบทความ … เสร็จแล้ว" ก่อนบรรทัด
SIGINT received— กด Ctrl+C ช้ากว่า 9 วินาที งานเลยเสร็จทัน · ลองใหม่ให้ไวขึ้น git status --shortยังเห็น?? test/jobs/— ยังลบไฟล์ test ไม่ครบ · ดูชื่อไฟล์ในtest/jobs/แล้วลบให้หมด (ห้ามใช้git restore .หรือgit cleanแบบทั้งโฟลเดอร์ ถ้ายังมีงานอื่นที่ไม่ได้ commit)- กดบันทึกแล้วได้หน้า error · log มี
NameError (uninitialized constant PostsController::SlowWorkJob)— ลบไฟล์ job ไปแล้วแต่ controller ยังเรียกอยู่ · ทำบรรทัดแรกของข้อ 5 (git restore) · ลองแล้ว: บทความถูกบันทึกไปแล้วก่อนพัง (พังตอนแขวนใบงาน หลัง COMMIT) ลบทิ้งด้วยปุ่มได้เลย
ทดสอบความเข้าใจ
ใน action create ของบทความ งานไหนควรย้ายไปเป็น background job?
ตัวเลือกที่ 3 ถูก — อีเมลยังไม่ออก ผู้ใช้ก็เห็นหน้าบทความได้ตามปกติ
ตัวเลือกที่ 1 ผิด — ไม่มีบทความในตู้ หน้าถัดไปก็ไม่มีอะไรให้ดู (และจริง ๆ ใช้แค่ไม่กี่ ms — log บอก ActiveRecord: 9.4ms) ·
ตัวเลือกที่ 2 ผิด — ต้องรู้ผลก่อนว่าจะโชว์หน้าบทความหรือโชว์ error ·
ตัวเลือกที่ 4 ผิด — redirect ต้องรู้ URL ตอนตอบ จะทำทีหลังไม่ได้
จุดที่ต้องจำ: ถามว่า "ถ้างานนี้ยังไม่เสร็จ ผู้ใช้เห็นหน้าถัดไปได้ไหม" · ได้ = ส่งเข้า job · ไม่ได้ = ทำใน request
development ใช้ adapter async · job กำลังทำได้ครึ่งทาง แล้วคุณกด Ctrl+C ที่ bin/dev แล้วเปิดใหม่ · เกิดอะไรขึ้น?
ตัวเลือกที่ 2 ถูก — ภารกิจพิสูจน์แล้ว: ไม่มีบรรทัด "เสร็จแล้ว" · เปิดใหม่ 25 วินาทีไม่มี [ActiveJob] · แต่บทความอยู่
ตัวเลือกที่ 1 ผิด — async เก็บใบงานในหน่วยความจำ ปิดโปรแกรมแล้วไม่มีที่ไหนจำ · ตัวเลือกที่ 3 ผิด — บทความ COMMIT ไปแล้วก่อนแขวนใบงาน สองเรื่องนี้ไม่ผูกกัน ·
ตัวเลือกที่ 4 ผิด — log บอก waiting for requests to finish คือรอ request ไม่ใช่ job (และ log ยังขึ้น Performed … in 2158.22ms ทั้งที่งานไม่ครบ)
จุดที่ต้องจำ: async = go f() ของ Rails · เร็ว ใช้ง่าย แต่โปรแกรมดับ = งานหาย → ห้ามใช้ใน production
คิวแบบ at-least-once · worker ส่งอีเมลสำเร็จ แล้วดับไปก่อนบอกคิวว่าเสร็จ · ผลคืออะไร?
ตัวเลือกที่ 4 ถูก — คือ t2 → t4 → t7 ในแผนภาพ · ช่องว่างระหว่าง "ทำแล้ว" กับ "บอกแล้ว" ปิดไม่ได้
ตัวเลือกที่ 1 ผิด — อีเมลออกไปแล้วตั้งแต่ t2 การยืนยันกับคิวไม่เกี่ยวกับอีเมล · ตัวเลือกที่ 2 ผิด — คิวรู้แค่ว่ายังไม่มีใครบอกว่าเสร็จ ไม่รู้ว่าข้างนอกเกิดอะไร · ตัวเลือกที่ 3 ผิด — อีเมลที่ส่งออกไปแล้วเรียกคืนไม่ได้ (ที่ไม่เหมือนกันของภาพพนักงานส่งของ)
จุดที่ต้องจำ: at-least-once = ไม่หายแต่อาจซ้ำ · คิวเกือบทุกตัวเลือกแบบนี้ → job ต้องทนการทำซ้ำได้
เพื่อนเสนอว่า "งั้นให้ worker บอกคิวว่าเสร็จก่อน แล้วค่อยส่งอีเมล จะได้ไม่ซ้ำ" · ข้อไหนอธิบายถูก?
ตัวเลือกที่ 1 ถูก — สลับลำดับ = เปลี่ยนจาก at-least-once เป็น at-most-once · ย้ายปัญหาจาก "ซ้ำ" ไปเป็น "หาย"
ตัวเลือกที่ 2 ผิด — ช่องว่างยังอยู่ แค่ย้ายไปอยู่อีกฝั่ง · ตัวเลือกที่ 3 ผิด — คิวได้ยินว่าเสร็จแล้วจึงไม่แจกซ้ำ ปัญหาคือหาย ไม่ใช่ซ้ำ · ตัวเลือกที่ 4 ผิด — ลำดับนี้เป็นเรื่องการออกแบบคิว ไม่ใช่กฎบังคับของ Rails · ประเด็นคือไม่ว่าลำดับไหน "พอดีครั้งเดียว" ก็ไม่มี
จุดที่ต้องจำ: เลือกได้แค่ว่าจะเจ็บทางไหน · งานที่หายไม่ได้ → ยอมซ้ำ แล้วออกแบบให้ "ซ้ำแล้วไม่เป็นไร" (บท 143)
ถามกลับได้เลย
ติดตรงไหนถามได้ทุกเรื่อง เช่น "ถ้า job ไปแก้บทความที่ผู้ใช้เพิ่งลบไปแล้วจะเกิดอะไร" · "แล้ว job ที่พังกลางทางไปไหน" (บท 144 ตอบเต็ม ๆ) · หรือวาง log ของคุณมาให้ดูด้วยกัน
ถ้าเปิดสไลด์ดูประกอบ
| หน้า | สไลด์เขียน | ของจริง (Rails 8.1.4 · แอปของคอร์ส) | ต้องสนใจไหม |
|---|---|---|---|
| 15 | "Solid Queue — ทำไม Rails 8 ใช้ตารางใน DB เป็นคิว" | บทนี้ยังเป็น async (development ค่าเริ่มต้น) · production ของเราตั้ง :solid_queue ไว้แล้ว · ติดตั้งจริงบท 143 | ไม่ต้อง |
| 16 | NotifyMailer.new_post(@post).deliver_now # 3 วิ · SearchIndex.push · variant(...).processed | แอปเรายังไม่มี mailer และระบบค้นหา · ตัวเลข 3/2/4 วิ เป็นตัวอย่าง · บทนี้จำลองด้วย sleep และวัดได้ 9,049 ms จริง | ไม่ต้อง — อย่าพิมพ์ตาม (ได้ uninitialized constant) |
| 16 | NotifyFollowersJob.perform_later(@post) (ส่งทั้งแฟ้ม) | บทนี้ส่ง @post.id · เหตุผลอยู่ในบท 143 | ยังไม่ต้อง |
| 16 | "งานที่อยู่ใน job พังแล้ว retry ได้" | จริงเมื่อคิวเก็บงานถาวรและตั้งให้ลองใหม่ (บท 143–144) · ตอนนี้ (async) ปิดโปรแกรมแล้วงานหาย — ภารกิจพิสูจน์แล้ว | สนใจ |
| 16 | "ย้อนกลับไปวันพฤหัส … เดี๋ยววันเสาร์จะเล่าว่า job คืออะไร" | เว็บนี้คือ Day 2 บท 112 (บทนั้นเขียนว่า "Day 3") → ตอนนี้ Day 4 | ไม่ต้อง |
| 17 | "ค่าเริ่มต้นของคิวเกือบทุกตัวในโลก" (at-least-once) | บทนี้ไม่ได้ตรวจคิวทุกตัว · เขียนว่า "สไลด์อ้าง" · ส่วน Solid Queue ดูในบท 143–144 | ไม่ต้อง |
อ้างอิง
- Rails Guides — Active Job Basics § What is Active Job? — อ่านสองย่อหน้าแรก: เป้าหมายของ background job คือย้ายงานยาว ๆ ออกจากวงจร request–response ให้หน้าเว็บตอบไว · แล้วอ่านหัวข้อ "Creating Jobs" ต่อ (ตรงกับขั้น generate ของบทนี้)
- Rails Guides — Active Job Basics § Development Environment — ย่อหน้าเดียว: adapter ของ development เก็บงานในหน่วยความจำ · process ดับหรือเครื่องรีเซ็ต งานที่ค้างหายหมด — สิ่งที่ภารกิจพิสูจน์
- Rails API — ActiveJob::QueueAdapters::AsyncAdapter — อ่านแค่ย่อหน้าแรก: ทำ job ด้วยกลุ่มคนงานในโปรแกรมเดียวกัน · เหมาะกับ dev/test · ไม่เหมาะกับ production เพราะทิ้งงานค้างตอน restart
- The Twelve-Factor App — IX. Disposability — อ่านย่อหน้าที่ขึ้นต้นด้วย "For a worker process": ตอนปิดอย่างสุภาพ worker ต้องคืนงานเข้าคิว · ทุก job จึงต้องทนการทำซ้ำ (idempotent) · และย่อหน้าถัดไปเรื่อง "sudden death"
- rails/solid_queue (README) — ยังไม่ต้องอ่านตอนนี้ · เก็บไว้เปิดคู่กับบท 143 (คิวที่เก็บงานในฐานข้อมูล ปิดโปรแกรมแล้วงานไม่หาย)