dimesum

หน้าแรก / บล็อก / เงิน

เงิน

ทำไมการแก้ไขค่าใช้จ่ายร่วมต้องระบุการแบ่งใหม่

· อ่าน 5 นาที ·

ค่าเริ่มต้นของการแก้ไขคือบั๊กเรื่องเงิน Dimesum จึงบังคับ participants และ split_type ในทุก PATCH ของค่าใช้จ่าย พร้อม base_version ที่ปฏิเสธการแก้ไขที่ล้าสมัยแทนที่จะรวมมัน

การแก้คำผิดไม่ควรเปลี่ยนว่าใครเป็นหนี้ใคร ใน Dimesum การแก้ไขค่าใช้จ่ายร่วมจะระบุการแบ่งทั้งหมดใหม่ participants และ split_type จำเป็นต้องมีในทุก PATCH และการแก้ไขที่ขาดฟิลด์ใดฟิลด์หนึ่งจะได้ 400 กลับมาแทนที่จะเติมค่าเริ่มต้นให้ ค่าเริ่มต้นตอนสร้างคือสมาชิกปัจจุบันทุกคนและการแบ่งเท่ากัน ซึ่งเมื่อใช้กับการแก้ไขแล้วกลายเป็นบั๊กเรื่องเงิน

ความล้มเหลวสองกรณีทำให้เห็นภาพชัด เพื่อนร่วมห้องที่เพิ่งย้ายเข้ามาในเดือนสิงหาคมถูกดึงเข้าไปในมื้อค่ำของเดือนกรกฎาคม เพราะ "สมาชิกปัจจุบันทุกคน" ถูกประเมินตอนที่การแก้ไขเข้ามา ไม่ใช่ตอนที่บันทึกค่าใช้จ่าย การแบ่งค่าเช่าแบบตั้งใจ 70/30 ถูกทำให้แบนเป็น 50/50 เพราะการไม่มี split_type หมายถึง EQUAL ทั้งสองกรณีไม่มีการแจ้งข้อผิดพลาด และทั้งคู่ย้ายเงินจริง

จุดยืน

ค่าเริ่มต้นของการแก้ไขคือบั๊กเรื่องเงิน ค่าเริ่มต้นตอนสร้างคือการเดาเกี่ยวกับกลุ่มที่ผู้บันทึกกำลังดูอยู่ในขณะนั้น การแก้ไขมาถึงทีหลัง กับกลุ่มที่เปลี่ยนไปแล้ว และการเดาเดียวกันก็เขียนทับสิ่งที่แต่ละคนเป็นหนี้อย่างเงียบ ๆ

ค่าเริ่มต้นตอนสร้างอธิบายค่าใช้จ่ายใหม่ ไม่ใช่ค่าใช้จ่ายเก่า

ตอนสร้าง ค่าเริ่มต้นนั้นตรงไปตรงมา ผู้บันทึกกำลังดูกลุ่มตามที่เป็นอยู่ และการแบ่งเท่ากันให้ทุกคนคือกรณีที่พบบ่อย Dimesum จึงเติมทั้งสองให้ ค่าใช้จ่ายจะเก็บอินพุตของมันไว้ ไม่ใช่แค่ผลลัพธ์ splits.percent_bp splits.weight และ splits.exact_minor เก็บสิ่งที่ผู้ใช้พิมพ์ ดังนั้นการแก้ไขในภายหลังจึงเปิดมันขึ้นมาใหม่ได้

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

สองเวอร์ชันของการแก้ไขเดียวกัน ค่าเริ่มต้นที่รับมาเปลี่ยนทุกส่วนแบ่ง ในขณะที่การแบ่งที่ระบุใหม่เปลี่ยนเฉพาะคำอธิบาย การแก้ไขรับค่าเริ่มต้นตอนสร้าง การแก้ไขระบุการแบ่งใหม่ ค่าเช่า 20,000 บาท กรกฎาคม ค่าเช่า 20,000 บาท กรกฎาคม v1 Asha 70%, 14,000 Bhavna 30%, 6,000 v1 Asha 70%, 14,000 Bhavna 30%, 6,000 Chetan ย้ายเข้าห้องในเดือนสิงหาคม Chetan ย้ายเข้าห้องในเดือนสิงหาคม PATCH แก้คำผิด ไม่ส่งการแบ่ง PATCH แก้คำผิด ส่งการแบ่ง participants: Asha, Bhavna split_type: PERCENT 7000 / 3000 v2 Asha 6,666.67 Bhavna 6,666.67 Chetan 6,666.66 v2 Asha 70%, 14,000 Bhavna 30%, 6,000 Chetan เป็นหนี้เดือนที่เขาไม่เคยอยู่ เปลี่ยนเฉพาะคำอธิบาย
การแก้ไขอักขระเดียวเดียวกัน สองครั้ง การรับค่าเริ่มต้นตอนสร้างมาแบ่ง 20,000 บาท สามทาง และคิดเงินสมาชิกที่ย้ายเข้ามาหนึ่งเดือนถัดมา การระบุการแบ่งใหม่ทำให้ทุกส่วนแบ่งไม่ถูกแตะต้อง

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

API ปฏิเสธการแก้ไขที่ไม่ระบุการแบ่งใหม่

การตรวจสอบที่เกตเวย์ทำงานก่อนการคำนวณเงินใด ๆ เมื่อ participants ว่างเปล่าหรือ split_type ไม่มีค่า คำขอจะได้ 400 กลับมาพร้อมโค้ด invalid_expense และข้อความ "an edit must restate the split: participants and split_type are required" บริการค่าใช้จ่ายย้ำกฎเดียวกันใน validateAmend ดังนั้นผู้เรียกที่เข้าถึงบริการด้วยวิธีอื่นก็จะพบการปฏิเสธเดียวกัน

สิ่งที่การสร้างส่ง เทียบกับสิ่งที่การแก้ไขต้องระบุใหม่ บน POST และ PATCH /v1/groups/{id}/expenses
ฟิลด์ตอนสร้างตอนแก้ไขค่าเริ่มต้นจะทำให้เสียอะไร
participantsไม่บังคับ ค่าเริ่มต้นเป็นสมาชิกปัจจุบันทุกคนบังคับสมาชิกที่ย้ายเข้ามาภายหลังเข้าร่วมค่าใช้จ่ายเก่า
split_typeไม่บังคับ ค่าเริ่มต้นเป็น EQUALบังคับการแบ่ง 70/30 ถูกทำให้แบนเป็น 50/50
payersไม่บังคับ ค่าเริ่มต้นเป็นผู้บันทึกการละไว้จะคงผู้จ่ายคนเดียวของค่าใช้จ่าย ผู้จ่ายสองคนต้องระบุใหม่ผู้แก้ไขกลายเป็นผู้จ่าย พลิกกลับว่าใครเป็นหนี้ใคร
base_versionไม่ต้องส่งบังคับ และต้องเท่ากับเวอร์ชันปัจจุบันการแก้ไขที่ล้าสมัยเขียนทับการเปลี่ยนแปลงที่ผู้บันทึกไม่เคยอ่าน
revision_idUUIDv7 จากไคลเอนต์ คีย์ idempotencyเหมือนกัน หนึ่งตัวต่อหนึ่งเวอร์ชันการแก้ไขที่ลองใหม่คิดเงินกลุ่มสองครั้ง
currencyระบุต่อค่าใช้จ่ายต้องตรงกัน การเปลี่ยนแปลงจะถูกปฏิเสธแถวยอดคงเหลือเก็บหนึ่งสกุลเงินต่อสมาชิกหนึ่งคน

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

การแก้ไขที่ล้าสมัยถูกปฏิเสธคืนให้ผู้บันทึก ไม่มีการรวม

base_version คืออีกครึ่งหนึ่งของสัญญา ทุก PATCH พกเวอร์ชันที่ผู้บันทึกอ่านมา และ checkTransition เทียบมันกับแถวที่ทรานแซกชันเพิ่งล็อก ถ้าเท่ากัน การแก้ไขจะมีผลที่เวอร์ชันบวกหนึ่ง ถ้าต่างกัน ผู้เรียกจะได้ HTTP 409 พร้อมโค้ด stale_version

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

ผู้บันทึกสองคนอ่านเวอร์ชัน 3 การแก้ไขแรกโพสต์เวอร์ชัน 4 และการแก้ไขที่สองถูกปฏิเสธด้วย 409 stale_version expense, version 3 ผู้บันทึก A PATCH base_version: 3 200 OK, version 4 ผู้บันทึก B PATCH base_version: 3 409 stale_version บัญชีแยกประเภท หนึ่งทรานแซกชัน EXPENSE_REVERSAL of v3 EXPENSE v4 ผู้บันทึก B อ่านเวอร์ชัน 4 อีกครั้ง ใช้การเปลี่ยนแปลงซ้ำ ส่ง base_version: 4
Optimistic concurrency บนค่าใช้จ่าย การแก้ไขที่แพ้ถูกส่งกลับไปให้ผู้บันทึกพร้อมเวอร์ชันที่มันต้องสร้างใหม่บน การแก้ไขที่ชนะไปถึงบัญชีแยกประเภทเป็นการกลับรายการบวกการโพสต์ซ้ำ ไม่ใช่การอัปเดต

Idempotency และ concurrency ถูกแยกออกจากกันอย่างตั้งใจ การแทรก revision ทำงานก่อนการตรวจเวอร์ชัน เพราะการแก้ไขที่ลองใหม่พก base version ที่มันอ่านมาแต่แรก ซึ่งตอนนี้ล้าสมัยแล้ว การเล่นซ้ำต้องถูกอ่านว่าเป็นการเล่นซ้ำ ไม่ใช่ข้อขัดแย้ง ดังนั้น revision_id จึงตอบก่อนและคืนผลลัพธ์ที่เก็บไว้

การตอบกลับมีข้อมูลที่การแก้ไขครั้งถัดไปต้องใช้อยู่แล้ว

การบังคับฟิลด์เพิ่มบน PATCH จะยุติธรรมก็ต่อเมื่อไคลเอนต์ได้มันมาอย่างง่าย ทุกการตอบกลับของค่าใช้จ่ายพก version ดังนั้นไคลเอนต์ที่เพิ่งเขียนค่าใช้จ่ายจึงแก้ไขได้โดยไม่ต้องอ่านครั้งที่สอง การตอบกลับของการสร้าง การตอบกลับของการแก้ไข และทุกแถวในรายการ พกฟิลด์เดียวกัน

สำหรับไคลเอนต์ที่ไม่ได้เพิ่งเขียนค่าใช้จ่าย GET /v1/groups/{id}/expenses/{id} จะคืนเวอร์ชัน ส่วนแบ่งที่คำนวณแล้ว และอินพุตที่อยู่เบื้องหลัง อินพุตกลับมาภายใต้ชื่อเดียวกับที่ PATCH รับ ได้แก่ participants split_type percents weights shares items pools ไคลเอนต์อ่านรูปแบบเดียวแล้วส่งกลับพร้อมการแก้ไข แทนที่จะต้องแปลระหว่างสองศัพท์สำหรับบิลเดียว

การตอบกลับ GET และคำขอ PATCH ใช้ชื่อฟิลด์เดียวกัน โดย version ถูกเปลี่ยนชื่อเป็น base_version GET คืนค่า PATCH รับ version base_version participants participants split_type split_type percents, weights, shares percents, weights, shares items, pools items, pools เปลี่ยนชื่อ
หนึ่งศัพท์ สองทิศทาง มีเพียงฟิลด์ version ที่เปลี่ยนชื่อตลอดการเดินทางไปกลับ ดังนั้นไคลเอนต์ที่แก้ไขจึงไม่ต้องดูแลชั้นการแปลระหว่างสิ่งที่มันอ่านและสิ่งที่มันเขียน

ความสมมาตรสำคัญที่สุดสำหรับการแบ่งที่สร้างใหม่ไม่ได้ การแบ่งแบบ PERCENT หรือบิลที่แจกแจงรายการไม่สามารถระบุใหม่จากส่วนแบ่งที่คำนวณแล้วเพียงอย่างเดียว เพราะได้ปัดเศษไปแล้วและ basis points กับรายการย่อยหายไปแล้ว สแนปช็อตของ revision พกอินพุตไว้ด้วย ดังนั้นแผ่นประวัติจึงแสดงได้ว่ามีอะไรถูกแจกแจงในเวอร์ชันก่อนหน้าใด ๆ

บังคับตั้งแต่ตอนนี้ เพราะข้อบังคับไม่สามารถเพิ่มภายหลังได้

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

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

ให้การแก้ไขระบุความหมายของมันใหม่

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

คำถามที่พบบ่อย

ทำไมแก้ไขค่าใช้จ่ายร่วมต้องกรอก participants และ split_type?

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

ถ้าสองคนแก้ไขค่าใช้จ่ายเดียวกันพร้อมกันจะเกิดอะไรขึ้น?

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

การแก้ไขค่าใช้จ่ายอัปเดตแถวบัญชีแยกประเภทในที่เดิมไหม?

ไม่ การแก้ไขค่าใช้จ่ายไม่เคยอัปเดตแถวบัญชีแยกประเภทใน Dimesum การแก้ไขจะโพสต์สองสมุดรายวันในหนึ่งทรานแซกชัน คือ EXPENSE_REVERSAL ที่หักล้างเวอร์ชันเก่าทีละขา แล้วตามด้วยสมุดรายวัน EXPENSE สำหรับเวอร์ชันใหม่ UPDATE และ DELETE ถูกเพิกถอนจาก database role ของบัญชีแยกประเภทเอง ดังนั้นการเปลี่ยนแปลงข้อมูลจึงเป็นไปไม่ได้แม้แต่กับโค้ดที่มีบั๊ก คุณจะเห็นป้ายว่าถูกแก้ไขและแผ่นประวัติ

ต้องเรียก API ครั้งที่สองก่อนแก้ไขค่าใช้จ่ายร่วมไหม?

ไม่ ไคลเอนต์ที่เพิ่งเขียนค่าใช้จ่ายมีเวอร์ชันที่การแก้ไขต้องใช้อยู่แล้ว ทุกการตอบกลับของค่าใช้จ่ายพก version ซึ่งเป็นสิ่งที่ PATCH ครั้งถัดไปส่งเป็น base_version ไคลเอนต์ที่ไม่ได้เขียนค่าใช้จ่ายจะเรียก GET /v1/groups/{id}/expenses/{id} ซึ่งคืนเวอร์ชันพร้อมอินพุตการแบ่งภายใต้ชื่อฟิลด์เดียวกับที่ PATCH รับ

ทำไมต้องบังคับ participants และ split_type ตั้งแต่ตอนนี้ แทนที่จะเพิ่มทีหลัง?

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