งูต้นฉบับกำลังส่งพวงกุญแจ cleanup จากตะขอชั่วคราวไปยังกล่องผู้รับผิดชอบใบใหม่

Python ExitStack.pop_all: โอนสิทธิ์ cleanup เมื่อทุกขั้นเตรียมสำเร็จ

พี่โชว์Engineering

การเปิด resource หลายชิ้นมีช่วงเสี่ยง: ชิ้นแรกสำเร็จแต่ชิ้นถัดไปล้มเหลว เราต้องปิดสิ่งที่เปิดแล้วทั้งหมดครับ ExitStack ลงทะเบียน cleanup ทีละขั้น และ pop_all() โอน callback ทั้งชุดไป stack ใหม่เมื่อ setup สำเร็จครบ ทำให้ transaction ของ resource ชัดเจน

WHY: ฝากกุญแจไว้กับเคาน์เตอร์จนตรวจห้องครบ

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

from contextlib import ExitStack
import unittest


def prepare(names, fail_at=None):
    events = []
    with ExitStack() as staging:
        for name in names:
            events.append(f"open:{name}")
            staging.callback(events.append, f"close:{name}")
            if name == fail_at:
                raise RuntimeError(name)
        owner = staging.pop_all()
    return events, owner


class ExitStackOwnershipTests(unittest.TestCase):
    def test_pop_all_defers_cleanup_to_new_owner(self):
        events, owner = prepare(["a", "b"])
        self.assertEqual(events, ["open:a", "open:b"])
        owner.close()
        self.assertEqual(events, ["open:a", "open:b", "close:b", "close:a"])

    def test_failure_closes_resources_in_reverse_order(self):
        with self.assertRaises(RuntimeError):
            prepare(["a", "b"], fail_at="b")


if __name__ == "__main__":
    unittest.main()

หลังโอนแล้วต้องมี owner ที่มองเห็นได้

pop_all ไม่เรียก cleanup เพียงย้าย callback ออก หากทิ้ง stack ใหม่ resource จะรั่วโดยการออกแบบ ควรคืน owner ออกจาก factory, ผูกกับ object ที่มี close หรือใช้ context manager ชั้นนอก และทำ close ให้ idempotent เท่าที่ resource รองรับ

Callback ทำงานย้อนลำดับเหมือน nested with จึงลงทะเบียนทันทีหลัง acquire อย่ารอท้ายฟังก์ชัน หาก cleanup เองล้มเหลวให้กำหนด log/exception policy ที่ไม่กลบสาเหตุหลักโดยไร้ร่องรอย

Recap

  • ลงทะเบียน cleanup ทันทีหลัง acquire
  • ใช้ pop_all เมื่อ setup ครบและต้องโอน ownership
  • คืน owner ที่มีเส้นทาง close ชัดเจน
  • ทดสอบทั้ง success และ failure ระหว่างทาง