บทนำ

กลุ่มเป้าหมาย: นักพัฒนา Gradle ที่เคยพบพฤติกรรมการบิลด์ที่ "มหัศจรรย์" หรือไม่คาดคิด

ในการผจญภัยสร้างปลั๊กอินของเรา`site-baker`, เราได้ปฏิบัติตามแนวทาง TDD ที่เข้มงวด ทุกฟีเจอร์ได้รับการทดสอบและตรวจสอบความถูกต้อง และเราเดินหน้าต่อด้วยความมั่นใจ และแล้ววันหนึ่ง สิ่งที่ไม่คาดคิดก็เกิดขึ้น การบิลด์เริ่มมีพฤติกรรมเอาแน่เอานอนไม่ได้ การแก้ไขในลอจิกของปลั๊กอินหรือในไฟล์กำหนดค่าดูเหมือนจะถูกละเลย และการทดสอบเชิงฟังก์ชันของเรา ซึ่งเคยเชื่อถือได้ กลับล้มเหลวโดยไม่มีสาเหตุที่ชัดเจน

ปัญหาลักษณะนี้อาจทำให้หงุดหงิดได้อย่างเหลือเชื่อ มันทำให้เกิดคำถามถึงความน่าเชื่อถือของเครื่องมือและความถูกต้องของโค้ดของเรา หลังจากช่วงการดีบักอย่างหนักหน่วง ผู้รับผิดชอบถูกระบุตัวแล้ว:แคชการกำหนดค่าของ Gradle.

อาการ : บิลด์ผี

ปัญหานี้แสดงออกในหลายรูปแบบ:

  1. ฉันกำลังแก้ไขสตริงอักขระในงานหนึ่ง`println`, แต่สตริงเก่ายังคงแสดงผลเมื่อรัน

  2. ฉันกำลังเปลี่ยนค่าในไฟล์ของฉัน`managed-jbake-context.yml`, แต่ปลั๊กอินทำงานราวกับว่าไฟล์ไม่ได้ถูกแก้ไข

  3. การทดสอบเชิงฟังก์ชัน ซึ่งสร้างโปรเจกต์ทดสอบขึ้นมาแบบทันที กำลังล้มเหลว เพราะปลั๊กอินดูเหมือนจะตรวจไม่พบไฟล์การกำหนดค่าที่เพิ่งสร้างขึ้นใหม่

ทุกอย่างเป็นไปราวกับว่า Gradle กำลังเรียกใช้ "เวอร์ชันผี" ของบิลด์ของเรา โดยไม่สนใจการเปลี่ยนแปลงล่าสุดของเรา

การสำรวจ: แคชการกำหนดค่าคืออะไร?

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

หลักการนั้นง่าย:

  1. ในการรันครั้งแรก Gradle จะดำเนินการในระยะของการกำหนดค่า(การอ่าน`build.gradle.kts`, การสร้างงาน การจัดการการพึ่งพา) และสร้างกราฟของงาน

  2. เมื่อสิ้นสุดระยะนี้ Gradleทำให้กราฟของงานนี้เป็นอนุกรมและเก็บไว้ในแคช

  3. ในการรันครั้งถัดไป หากไม่มีอะไรเปลี่ยนแปลง (สคริปต์ build,gradle.properties, ฯลฯ), Gradleข้ามขั้นตอนการกำหนดค่าไปเลยและนำกราฟงานที่แคชไว้กลับมาใช้ใหม่

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

วงจรชีวิตของแคชการกำหนดค่า
@startuml
start
:การดำเนินการครั้งแรก;
:ระยะการกำหนดค่า;
:การสร้างกราฟของงาน;
:การแคชกราฟ;
:ระยะการดำเนินการ;
end

start
:การดำเนินการครั้งถัดไป;
if (Cache valide ?) then (oui)
  :การกู้คืนกราฟจากแคช;
  note right
ขั้นตอนการกำหนดค่าถูกข้ามไป!
  end note
else (non)
  :ระยะการกำหนดค่า;
  :การอัปเดตแคช;
endif
:ระยะการดำเนินการ;
stop
@enduml

สาเหตุของปัญหา: ปลั๊กอินที่ไม่เป็นไปตามข้อกำหนด

ปลั๊กอินของเรา`site-baker`โดยไม่รู้ตัวว่ากำลังละเมิดกฎหลายข้อของแคชการกำหนดค่า เพื่อให้กราฟงานสามารถซีเรียลไลซ์ได้ งานต้องไม่มีการอ้างอิงถึงออบเจ็กต์ที่ซับซ้อน เช่น ออบเจ็กต์`Project`หรืออ่านไฟล์ตามอำเภอใจในระหว่างขั้นตอนการรัน

ข้อผิดพลาดหลักของเราคือการอ่านเนื้อหาของไฟล์ YAML โดยตรงภายในลอจิกการทำงานของ task โดยใช้การอ้างอิงไปยังพาธที่จัดเก็บไว้ใน extension ของเรา แนวทางนี้เข้ากันไม่ได้กับแคช เนื่องจาก Gradle ไม่สามารถทราบได้ว่าเนื้อหาของไฟล์เปลี่ยนแปลงหรือไม่ หากการอ่านนี้ไม่ได้ถูกจำลองให้เป็นอินพุตงาน(อินพุตงาน)

วิธีแก้ไขชั่วคราว: ปิดใช้งานแคช

เพื่อให้เราหลุดจากสถานะที่ติดขัดและกลับมามีพฤติกรรมการบิลด์ที่คาดเดาได้ วิธีที่เร็วที่สุดคือการปิดใช้งานแคชการกำหนดค่า เพียงเพิ่มบรรทัดต่อไปนี้ในไฟล์`gradle.properties`ของโปรเจกต์ที่ใช้ปลั๊กอิน (หรือในกรณีของเรา โปรเจกต์ทดสอบ`site-baker`).

# site-baker/gradle.properties
org.gradle.configuration-cache=false

ทันทีนั้น บิลด์ก็กลับสู่พฤติกรรมปกติ การรันแต่ละครั้งเริ่มเฟสการกำหนดค่าใหม่ และการเปลี่ยนแปลงของเราถูกนำไปใช้

อย่างไรก็ตาม นี่เป็นเพียงวิธีเลี่ยงปัญหา ไม่ใช่แนวทางแก้ไขที่ยั่งยืน วิธีนี้แลกมาด้วยประสิทธิภาพและไม่ได้แก้ปัญหาพื้นฐานของปลั๊กอินของเรา

วิธีแก้ปัญหาที่แท้จริง: ทำให้ปลั๊กอินเข้ากันได้

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

กุญแจสำคัญคือการใช้API ของผู้ให้บริการของ Gradle แทนที่จะส่งค่าที่ระบุโดยตรง (เช่น`String` ou un File) สำหรับงานของเรา เราต้องใช้`Property<T>`หรือบาง`Provider<T>`.

นี่คือแผนการรีแฟคเตอร์ริง:
  1. ประกาศรายการงาน:งานที่พาร์สไฟล์ YAML ต้องประกาศไฟล์นี้เป็นอินพุต สำหรับสิ่งนี้จะใช้แอนโนเทชัน`@InputFile`.

[source,kotlin] ---- @get:InputFile abstract val configFile: RegularFileProperty ----

  1. ใช้ Property และ Provider :ค่าของ`configFile`จะถูกเชื่อมต่อกับทรัพย์สิน`configPath`ของส่วนขยาย DSL ของเรา Gradle จึงสามารถติดตามแหล่งที่มาของข้อมูลได้

[source,kotlin] ---- // ในปลั๊กอิน tasks.register<MyTask>("myTask") { configFile.set(extension.configPath.flatMap { project.layout.projectDirectory.file(it) }) } ----

  1. อ่านเนื้อหาในเวลาที่เหมาะสม:การอ่านไฟล์ต้องทำภายในแอ็กชันของทาสก์ (@TaskAction), โดยใช้`Provider`ของทางเข้า

[source,kotlin] ---- @TaskAction fun execute() { val content = configFile.get().asFile.readText() // …​ แยกวิเคราะห์เนื้อหา } ----

เมื่อปฏิบัติตามรูปแบบนี้ Gradle จะเข้าใจว่าหากเนื้อหาของ`configFile`เปลี่ยนแปลง, แคชการกำหนดค่าไม่ถูกต้อง และต้องดำเนินการขั้นตอนการกำหนดค่าใหม่ นอกจากนี้ ระบบรู้ว่าผลลัพธ์ของงานขึ้นอยู่กับเนื้อหาของไฟล์นี้ ซึ่งยังช่วยให้สามารถปรับแคชการดำเนินการให้เหมาะสมได้ด้วย (UP-TO-DATE).

บทสรุป

การผจญภัยในการดีบักครั้งนี้เป็นบทเรียนอันมีค่า พฤติกรรม "มหัศจรรย์" ของแคชการกำหนดค่าได้บังคับให้เราเข้าใจวงจรชีวิตของ Gradle และหลักการของการเขียนโปรแกรมเชิงประกาศและแบบเลื่อนเวลาได้ดียิ่งขึ้น (lazy).

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

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

บทความที่เกี่ยวข้อง