EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Web / บิลหายเงียบเพราะ sync ล้มแล้วถูก mark ว่าส่งแล้ว
Web

บิลหายเงียบเพราะ sync ล้มแล้วถูก mark ว่าส่งแล้ว

โดย Ton-Or ·6 พ.ย. 2026·อ่าน 10 นาที
บิลหายเงียบเพราะ sync ล้มแล้วถูก mark ว่าส่งแล้ว

คิว offline ที่อันตรายที่สุดไม่ใช่คิวที่ค้าง แต่เป็นคิวที่บอกว่าว่างแล้วทั้งที่ของยังไม่ถึงปลายทาง ความล้มเหลวที่ถูกกลืนหายไปแพงกว่าความล้มเหลวที่ค้างให้เห็นอยู่บนจอเสมอ

สรุปใน 1 นาที

อาการ/ปัญหา
คิว sync ของแอปขายหน้าร้าน offline retry ไม่มีเพดาน และพอเจอ 4xx กลับ mark ว่าส่งแล้ว บิลหายโดยไม่ถึงเซิร์ฟเวอร์
ใครควรอ่าน
นักพัฒนาแอป offline-first ที่มีคิวส่งข้อมูล เช่น POS
ทางแก้แบบสั้น
ใช้นโยบายเดียว เพดาน 5 ครั้ง backoff 1/5/30/120/300 วินาที แถวถาวรถูกจอดด้วยค่า 99 และแสดงบนหน้าจอ เซิร์ฟเวอร์ต้องรับของซ้ำได้
ข้ามไปขั้นตอน

พอดีผมกลับไปไล่อ่านชั้น sync ของแอปขายหน้าร้านตัวหนึ่งที่ผมทำไว้ เป็นแอป Android ที่บันทึกบิลลงฐานข้อมูลในเครื่องก่อน แล้วค่อย push ขึ้น API ทีหลัง เพราะร้านเป้าหมายอยู่ในตลาดที่เน็ตหลุดเป็นเรื่องปกติ อ่านไปถึงกิ่งที่จัดการ error แล้วเจอของที่ผมเขียนเองกับมือ คือถ้าเซิร์ฟเวอร์ตอบ 4xx โค้ดจะ mark แถวนั้นว่าส่งแล้ว แล้วเดินต่อ บิลใบนั้นหายจากตัวนับค้างส่ง คนขายเห็นว่าทุกอย่างขึ้นเซิร์ฟเวอร์หมดแล้ว ทั้งที่ฝั่งเซิร์ฟเวอร์ไม่เคยมีบิลใบนั้นเลย ผมเลยรื้อส่วนนี้ใหม่ทั้งชั้น แล้วเอาเหตุผลกับโค้ดมาเล่าไว้ตรงนี้

ชื่อคลาส ชื่อคอลัมน์ และตัวอย่างข้อมูลในบทความนี้ผมเขียนใหม่ให้เป็นกลาง แต่ตัวเลขนโยบาย ตารางรหัสตอบ และจำนวน test คือของจริง

พฤติกรรมเดิม: บิลหายได้สองทาง และเงียบทั้งสองทาง

โครงของแอปคือ Kotlin, Room, WorkManager กับ Retrofit ตารางบิลในเครื่องมีสองคอลัมน์ที่ใช้เป็นคิวไปในตัว คือ syncedAt (ว่าง = ยังไม่ได้ส่ง) กับ syncAttempts (ลองไปแล้วกี่ครั้ง) worker หยิบแถวที่ syncedAt ว่างมา push ทีละใบ ปัญหาอยู่ที่สองกิ่งของการจัดการ error

ทางแรก ถ้าเป็น error ชั่วคราว worker คืน retry ให้ WorkManager ไปเรื่อย ๆ ไม่มีเพดาน แถวที่ไม่มีวันสำเร็จก็วนอยู่ในคิวตลอดไป ตัวนับค้างส่งไม่เคยเป็นศูนย์ และไม่มีอะไรบอกคนขายว่าใบไหนคือตัวปัญหา

ทางที่สอง แย่กว่ามาก ถ้าเซิร์ฟเวอร์ตอบ 4xx โค้ดเดิมเรียก markSynced แล้วตามด้วย markSyncFailure คือบันทึกข้อความ error ไว้ แต่ตั้ง syncedAt ให้มีค่าไปด้วย แถวเลยหลุดจากคิวทันที

mark ว่าส่งแล้วเพื่อเคลียร์คิว คือการทิ้งข้อมูลเงิน

จากมุมคนขาย บิลที่ถูกเคลียร์แบบนี้ไม่ต่างจากบิลที่ส่งสำเร็จเลย ตัวนับค้างส่งเป็นศูนย์ ไม่มีสีแดง ไม่มีข้อความเตือน แต่ยอดขายของสาขาบนเซิร์ฟเวอร์ขาดไปหนึ่งใบ กว่าจะมีใครรู้คือตอนเจ้าของร้านเอารายงานปิดวันมาเทียบกับเงินในลิ้นชัก ซึ่งถึงตอนนั้นไม่มีร่องรอยให้ตามแล้วว่าใบไหนหาย

เหตุผลตอนเขียนรอบแรก ฟังดูดีแต่ผิดเป้า

ผมไม่ได้เขียนแบบนั้นเพราะสะเพร่า ตอนนั้นผมมีเหตุผลชัดเจน คือกลัวแถวพังหนึ่งแถวไปอุดคิวทั้งคิว ถ้าบิลใบหนึ่งโดน validation ปฏิเสธ มันจะโดนปฏิเสธซ้ำทุกรอบ ถ้าปล่อยให้ค้างอยู่หัวคิว บิลใบหลัง ๆ ก็ไม่ได้ไปต่อ ผมเลยเลือกเอาแถวนั้นออกจากคิวด้วยวิธีที่ง่ายที่สุดที่มีอยู่ในมือ คือ mark ว่าส่งแล้ว

ข้อผิดของผมคือเอาปัญหาสองเรื่องมาแก้ด้วยท่าเดียว เรื่องแรกคือ "worker ไม่ควรลองแถวนี้อีก" เรื่องที่สองคือ "แถวนี้จบแล้ว" สองอย่างนี้ไม่ใช่เรื่องเดียวกัน แถวที่เซิร์ฟเวอร์ปฏิเสธคือแถวที่ worker ควรหยุดลอง แต่มันยังไม่จบ มันต้องการคนมาดู

แบบเดิม

สองสถานะ: ยังไม่ส่ง กับส่งแล้ว

error ชั่วคราววนไม่มีเพดาน error ถาวรถูกยัดเข้าสถานะ "ส่งแล้ว" เพราะไม่มีสถานะอื่นให้ลง คิวดูสะอาด แต่ข้อมูลหายโดยไม่มีสัญญาณ

แบบใหม่

สามสถานะ: Pending, Stuck, Synced

error ชั่วคราวลองได้ไม่เกิน 5 ครั้ง error ถาวรถูกจอดเป็น Stuck ทันที worker ข้ามแถวที่จอดไว้ คิวเดินต่อได้ และคนขายเห็นว่ามีใบที่ต้องจัดการ

แยกนโยบายออกเป็น object ล้วนที่ไม่แตะ Android

ของเดิมกฎพวกนี้กระจายอยู่ใน worker ปนกับโค้ดเรียก API กับ DAO จะเขียน test ก็ต้องตั้ง WorkManager ตั้ง Room ขึ้นมาก่อน ผมเลยดึงกฎทั้งหมดออกมาเป็น object เดียวที่รับตัวเลขเข้า คืนคำตอบออก ไม่รู้จัก Context ไม่รู้จักฐานข้อมูล

object SyncPolicy {
    const val MAX_ATTEMPTS = 5

    // ถาวร = ลองอีกกี่ครั้งก็ได้คำตอบเดิม
    fun isPermanentError(code: Int): Boolean =
        code in 400..499 && code != 408 && code != 429

    fun shouldGiveUp(attempts: Int): Boolean = attempts >= MAX_ATTEMPTS

    // เส้น backoff เป็นวินาที ส่งต่อให้ตัวจัดตารางงานอีกที
    fun nextBackoffSeconds(attempts: Int): Long = when {
        attempts <= 1 -> 1L
        attempts == 2 -> 5L
        attempts == 3 -> 30L
        attempts == 4 -> 120L
        else -> 300L
    }

    sealed class State {
        data object Pending : State()
        data object Stuck : State()
        data object Synced : State()
    }

    fun classify(syncedAt: Long?, attempts: Int): State = when {
        syncedAt != null -> State.Synced
        shouldGiveUp(attempts) -> State.Stuck
        else -> State.Pending
    }
}

การจำแนกรหัสตอบคือหัวใจของ object นี้ รหัสที่เจอจริงในระบบนี้แบ่งได้ตามตาราง

รหัสตอบ ความหมายในระบบนี้ จัดเป็น สิ่งที่ worker ทำ
400, 422 ข้อมูลในบิลไม่ผ่าน validation ถาวร จอดทันที
401, 403 token หมดอายุ หรือไม่มีสิทธิ์ ถาวร จอดทันที
404 ปลายทางหรือสินค้าที่อ้างถึงไม่มี ถาวร จอดทันที
409 ชนกับข้อมูลที่มีอยู่ ถาวร จอดทันที
408, 429 timeout หรือโดนจำกัดอัตรา ชั่วคราว นับครั้ง รอแล้วลองใหม่
5xx และ exception ของเครือข่าย เซิร์ฟเวอร์หรือเน็ตมีปัญหา ชั่วคราว นับครั้ง รอแล้วลองใหม่

เส้น backoff คือ 1, 5, 30, 120 วินาที แล้วไปชนเพดานที่ 300 ซึ่งในทางปฏิบัติแถวจะถูกจอดก่อนถึงค่านั้น เพราะล้มครบ 5 ครั้งพอดี ตัวเลขชุดนี้เป็นนโยบายที่ป้อนให้ตัวจัดตารางงาน เวลาที่ worker ถูกปลุกจริงยังขึ้นกับ WorkManager กับสภาพเครื่องอีกชั้น

ผลพลอยได้ที่ผมชอบที่สุดคือไฟล์นี้ไม่มี import android สักบรรทัด test เลยรันบน JVM ธรรมดาได้ ไม่ต้องมีเครื่องหรือ emulator

แถวถาวรต้องถูกจอด ไม่ใช่ถูกเคลียร์

กิ่ง error ใน worker เหลือสามทางที่อ่านแล้วเข้าใจได้ในครั้งเดียว

val resp = api.pushSale(bearer, body)
when {
    resp.isSuccessful ->
        dao.markSynced(sale.localId, System.currentTimeMillis())

    SyncPolicy.isPermanentError(resp.code()) ->
        // จอด ไม่เคลียร์ และเก็บข้อความที่คนอ่านรู้เรื่องไว้กับแถว
        dao.markSyncStuck(sale.localId, readableError(resp))

    else ->
        // ชั่วคราว: attempts + 1 แล้วให้รอบถัดไปลองใหม่
        dao.markSyncFailure(sale.localId, "HTTP ${resp.code()}")
}

ผมไม่ได้เพิ่มตารางคิวใหม่ สองคอลัมน์เดิมพอแล้ว แค่เพิ่มความหมายให้ syncAttempts อีกค่าหนึ่ง คือ 99 เป็นค่า sentinel ที่แปลว่า "จอดเพราะ error ถาวร" มันมากกว่าเพดาน 5 อยู่แล้ว classify เลยจัดเป็น Stuck โดยไม่ต้องมีเงื่อนไขพิเศษ และ syncedAt ยังว่างเหมือนเดิม แถวไม่มีทางถูกนับว่าส่งแล้ว

-- แถวที่รอส่ง: worker หยิบชุดนี้แล้วกรองแถวที่ attempts >= 5 ออกอีกชั้น
SELECT * FROM sales WHERE syncedAt IS NULL ORDER BY id LIMIT 50;

-- จำนวนแถวที่ติด ใช้ขับ badge สีแดง (:max = 5)
SELECT COUNT(*) FROM sales WHERE syncedAt IS NULL AND syncAttempts >= :max;

-- จอดแถวถาวร: ตั้ง sentinel 99 เก็บ error ไม่แตะ syncedAt
UPDATE sales SET syncAttempts = 99, lastSyncError = :error WHERE localId = :localId;

-- ปุ่มลองอีก: คืนแถวเข้ารอบปกติ
UPDATE sales SET syncAttempts = 0, lastSyncError = NULL WHERE localId = :localId;
408 / 429 / 5xx: รอตาม backoff แล้วลองใหม่Pendingattempts ต่ำกว่า 52xx: accepted หรือ duplicatesSyncedsyncedAt มีค่ากดลองอีก: attempts = 04xx ถาวร หรือครบ 5 ครั้งStuckattempts = 99 หรือ 5badge แดง ติด N พร้อมข้อความ errorsyncedAt ยังว่าง แถวไม่หายจากจอเส้นเวลา backoff ของแถวที่ล้มแบบชั่วคราวล้มครั้งที่ 1ครั้งที่ 2ครั้งที่ 3ครั้งที่ 4ครั้งที่ 5รอ 1 วิรอ 5 วิรอ 30 วิรอ 120 วิStuck
แถวหนึ่งแถวมีสามสถานะ ทางเข้า Stuck มีสองทางคือ 4xx ถาวร (ตั้ง 99 ทันที) กับล้มแบบชั่วคราวครบ 5 ครั้ง ทางออกจาก Stuck มีทางเดียวคือคนกดลองอีก ระยะบนเส้นเวลาไม่ได้วาดตามสเกล
ทำไมใช้ 99 ไม่ใช้ 5

แถวที่ล้มครบ 5 ครั้งกับแถวที่เซิร์ฟเวอร์ปฏิเสธตั้งแต่ครั้งแรก ถูกจอดเหมือนกัน แต่ที่มาไม่เหมือนกัน ค่า 99 ทำให้แยกออกได้จากข้อมูลในแถวเอง ว่าใบนี้ติดเพราะเน็ตหรือเซิร์ฟเวอร์ล่มนานเกินไป (ลองใหม่น่าจะผ่าน) หรือติดเพราะเนื้อบิลมีปัญหา (กดลองใหม่เฉย ๆ จะได้คำตอบเดิม ต้องแก้ต้นเหตุก่อน)

ฝั่งเซิร์ฟเวอร์ต้องรับของซ้ำได้ ไม่งั้น retry คือการสร้างบิลซ้ำ

retry ทุกรูปแบบมีเงื่อนไขเดียวกัน คือปลายทางต้องรับของซ้ำได้โดยไม่เกิดผลซ้ำ กรณีที่เจอบ่อยในร้านที่เน็ตแย่คือ request ไปถึงเซิร์ฟเวอร์และถูกบันทึกแล้ว แต่ response หลุดกลางทาง เครื่องขายเห็นเป็น timeout แล้วส่งใหม่ ถ้าเซิร์ฟเวอร์ไม่กันไว้ จะได้บิลสองใบ

ส่วนนี้ backend ทำไว้ถูกอยู่แล้ว ผมไม่ต้องแก้อะไร ทั้งบิลและรายการเคลื่อนไหวสต็อกกันซ้ำด้วยคู่ (branch_id, local_id) โดย local_id เป็นรหัสที่เครื่องขายสร้างเองตอนบันทึก และไม่เปลี่ยนไม่ว่าจะส่งกี่รอบ

$accepted = [];
$duplicates = [];

foreach ($request->input('movements') as $m) {
    $seen = StockMovement::where('branch_id', $branch->id)
        ->where('local_id', $m['local_id'])
        ->exists();

    if ($seen) {
        $duplicates[] = $m['local_id'];   // เคยรับแล้ว ไม่สร้างซ้ำ
        continue;
    }

    StockMovement::create($m + ['branch_id' => $branch->id]);
    $accepted[] = $m['local_id'];
}

return response()->json(['accepted' => $accepted, 'duplicates' => $duplicates]);
{
  "accepted":   ["loc-0007", "loc-0008"],
  "duplicates": ["loc-0006"]
}

จุดสำคัญคือเซิร์ฟเวอร์ตอบแยกสองกลุ่ม และฝั่งเครื่องขาย mark synced ทั้งสองกลุ่ม เพราะ duplicate แปลว่าของถึงแล้วตั้งแต่รอบก่อน ไม่ใช่ error ถ้าไปตอบ duplicate เป็น 4xx มันจะวิ่งเข้ากิ่งถาวรแล้วถูกจอด ทั้งที่ข้อมูลอยู่ครบ เรื่องการจองสถานะก่อนทำงานฝั่งเซิร์ฟเวอร์ผมเขียนแยกไว้แล้วใน งานที่ยิงออกนอกระบบ ต้องจองสถานะก่อนยิง อันนั้นเป็นฝั่งเซิร์ฟเวอร์ยิงออก อันนี้เป็นฝั่งเครื่องลูกยิงเข้า หลักเดียวกันแต่คนละด้าน

อีกสัญญาหนึ่งที่มี test ยืนยัน คือเซิร์ฟเวอร์ปฏิเสธรายการสต็อกที่มีเหตุผลเป็น void จากเส้น push ของเครื่องขาย เพราะการ void ทำได้จากฝั่งเจ้าของร้านเท่านั้น ถ้าเครื่องขายส่งมา จะได้ 422 และแถวนั้นจะไปโผล่เป็น Stuck ให้เห็น ซึ่งเป็นพฤติกรรมที่ถูกต้อง

เส้นทางของบิลหนึ่งใบขายบันทึกลงเครื่อง->PendingsyncedAt ว่าง->pushพร้อม local_id->accepted / duplicatesmark synced ทั้งคู่->Synced

หน้าจอ: ความล้มเหลวต้องมีที่อยู่ให้คนเห็น

ส่วนที่แก้ปัญหาจริงไม่ใช่โค้ดฝั่ง worker แต่เป็นหน้าจอ เพราะแถวที่จอดไว้โดยไม่มีใครเห็นก็ไม่ต่างจากแถวที่ถูกเคลียร์ทิ้ง

badge สถานะ sync บนหน้าขายเดิมรับค่าเดียวคือจำนวนที่รอส่ง ตอนนี้รับสองค่า คือ pendingCount กับ stuckCount เมื่อ stuckCount มากกว่าศูนย์ badge เปลี่ยนเป็นสีแดงพร้อมข้อความแบบ "ติด N" แตะแล้วเปิด dialog ที่แสดงแถวที่ติดทีละแถว พร้อมข้อความ error ของแถวนั้นกับปุ่มลองอีก ปุ่มนี้ทำอย่างเดียวคือตั้ง syncAttempts กลับเป็น 0 แล้วให้ worker หยิบไปในรอบถัดไปตามปกติ

กรณี 401 เป็นตัวอย่างที่เห็นภาพง่าย token ของสาขาหมดอายุ บิลที่ push ช่วงนั้นถูกจอดทั้งชุด คนขาย login ใหม่ แล้วกดลองอีก แถวกลับเข้าคิว ของเดิมบิลชุดนั้นจะถูก mark ว่าส่งแล้วทั้งหมดโดยไม่มีใครรู้

ข้อความ error ต้องเป็นภาษาที่คนขายอ่านออก

ถ้า dialog แสดงแค่ "HTTP 422" คนหน้าร้านทำอะไรต่อไม่ได้นอกจากกดลองอีกซ้ำ ๆ ซึ่งจะได้ผลเดิมทุกครั้ง ผมให้ worker แกะ body ของ response ที่เซิร์ฟเวอร์ส่งมาเป็นโครงสร้าง แล้วเรียบเรียงเป็นประโยคก่อนเก็บลงแถว อย่างน้อยให้รู้ว่าปัญหาอยู่ที่สินค้าตัวไหนหรือสิทธิ์อะไร

ทดสอบ: 14 ตัวบน JVM กับ 6 ตัวที่ API

พอ logic อยู่ใน object ล้วน test ก็เขียนตรงไปตรงมา ชุดฝั่ง JVM เพิ่มจาก 29 เป็น 43 ตัว ที่เพิ่ม 14 ตัวคือ test ของนโยบายล้วน ๆ ครอบสี่กลุ่ม คือ isPermanentError ทีละรหัส shouldGiveUp ที่ขอบ 4 กับ 5 เส้น backoff ทุกขั้น และ state machine ของ classify

@Test fun permanentVsTransient() {
    listOf(400, 401, 403, 404, 409, 422).forEach { assertTrue(SyncPolicy.isPermanentError(it)) }
    listOf(408, 429, 500, 502, 503).forEach { assertFalse(SyncPolicy.isPermanentError(it)) }
}

@Test fun sentinelIsStuckNotSynced() {
    assertEquals(SyncPolicy.State.Stuck, SyncPolicy.classify(syncedAt = null, attempts = 99))
    assertEquals(SyncPolicy.State.Pending, SyncPolicy.classify(syncedAt = null, attempts = 4))
    assertEquals(SyncPolicy.State.Synced, SyncPolicy.classify(syncedAt = 1L, attempts = 99))
}

ฝั่ง API เพิ่มจาก 82 เป็น 88 ตัว 6 ตัวที่เพิ่มยืนยันสัญญาของเส้น push รายการสต็อก คือรับของใหม่ กันของซ้ำ batch ที่ผสมทั้งใหม่และซ้ำ ตรวจค่าเหตุผลที่อนุญาต และการยืนยันตัวตน

# ฝั่งแอป: test นโยบาย รันบน JVM ไม่ต้องมีเครื่อง
./gradlew testDebugUnitTest --tests '*SyncPolicyTest'

# ฝั่ง backend: สัญญาของเส้น push
php artisan test --filter=StockMovementsPush

ส่วนหน้าจอของแถวติด ผมยังไม่ได้เขียน test อัตโนมัติ ใช้ทดสอบมือตามลำดับนี้

  1. ทำให้เกิด error ถาวรหนึ่งแถว วิธีง่ายสุดคือใส่ product_id ที่ไม่มีจริงในรายการสต็อก แถวนั้นต้องถูกจอดตั้งแต่ครั้งแรก ไม่วนลองซ้ำ
  2. ทำให้เกิด error ชั่วคราว โดยปิด backend ระหว่างที่เครื่องกำลัง push แล้วรอจนครบ 5 ครั้ง
  3. ดู badge บนหน้าขาย ต้องเปลี่ยนเป็นสีแดงพร้อมจำนวนแถวที่ติด และตัวนับรอส่งต้องไม่รวมแถวพวกนี้
  4. แตะ badge ต้องเห็นแถวทีละใบพร้อมข้อความ error ที่อ่านรู้เรื่อง
  5. แก้ต้นเหตุ (เปิด backend กลับ หรือแก้ข้อมูล) แล้วกดลองอีก แถวต้องกลับไปเป็น Pending และหายจากรายการติดเมื่อส่งผ่าน
  6. เช็กที่ฝั่งเซิร์ฟเวอร์ว่าบิลหรือรายการนั้นมีใบเดียว ไม่ซ้ำ แม้จะถูกส่งหลายรอบ
5 ครั้งเพดานลองใหม่ต่อแถว
1/5/30/120วินาทีที่รอระหว่างครั้ง
99sentinel ของแถวถาวร
29 -> 43test ฝั่ง JVM
82 -> 88test ฝั่ง API
0 ตารางที่ต้องเพิ่มในเครื่อง

ของที่ตั้งใจยังไม่ทำ

มีสามเรื่องที่ผมรู้ว่ายังขาด และเลือกไม่ทำในรอบนี้

เรื่องแรก 409 ยังถูกนับเป็นถาวรแล้วติดเหมือน 4xx ตัวอื่น ในหลายกรณี 409 แปลว่าของมีอยู่แล้วที่ปลายทาง ซึ่งคำตอบที่ถูกคือถือว่าส่งแล้ว แต่ตอนนี้ยังไม่มีปุ่ม "นี่คือรายการซ้ำ ให้ถือว่าส่งแล้ว" ใน dialog แถวแบบนี้เลยค้างเป็นสีแดงจนกว่าจะมีคนมาดู ผมยอมให้ค้างดีกว่าให้ระบบตัดสินใจเคลียร์เอง เพราะนั่นคือบั๊กเดิมที่เพิ่งแก้ไป

เรื่องที่สอง ไม่มี log ประวัติการ sync ในเครื่อง แถวเก็บแค่ error ครั้งล่าสุด ถ้าอยากรู้ว่าห้าครั้งที่ล้มนั้นล้มเพราะอะไรบ้างตามลำดับ ตอนนี้ดูไม่ได้

เรื่องที่สาม ไม่มีตารางคิวแยก ซึ่งข้อนี้ผมตั้งใจ เพราะสองคอลัมน์เดิมให้ความหมายของคิวครบแล้ว เพิ่มตารางคือเพิ่มที่ให้ข้อมูลสองชุดไม่ตรงกัน

สรุปคือกติกาที่ผมเอาไปใช้กับคิว offline ทุกตัวหลังจากนี้ มีข้อเดียว แถวออกจากคิวได้สองทางเท่านั้น คือปลายทางยืนยันว่าได้รับ หรือคนเป็นคนตัดสินใจ worker ไม่มีสิทธิ์ตัดสินว่าแถวไหนจบเพียงเพราะมันส่งไม่ผ่าน ใครจะเอาตัวเลขเพดานกับเส้น backoff ไปเปลี่ยนให้เข้ากับงานตัวเองก็ตามสะดวกครับ ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

อยากให้เราทำให้ไหม?

ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า

คุยเรื่องงานกับเรา

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย