Steven
Steven8 phút đọc

Cơ chế kiểm tra an toàn khiến ứng dụng của chúng tôi không thể tắt nổi

Auto-update là tính năng khó ship nhất mà chúng tôi từng làm — sáu bản phát hành trong bốn ngày chỉ để nó ngừng làm hỏng ứng dụng. Con bug tệ nhất lại chính là thứ chúng tôi tự tạo ra khi cố gắng cẩn thận: một bước kiểm tra "an toàn" dài 500 mili-giây biến một bản cập nhật thất bại thành một tiến trình mà bạn thực sự không thể tắt được.

Kỹ thuật
Electron
Độ tin cậy
Bản phát hành GeekBye
Cơ chế kiểm tra an toàn khiến ứng dụng của chúng tôi không thể tắt nổi

Mọi lập trình viên ứng dụng desktop đều đánh giá thấp auto-update đúng một lần. Nó trông giống một bài toán đã được giải xong — một thư viện tải về phiên bản mới và khởi động lại ứng dụng của bạn. Rồi bạn ship nó, và bạn học được rằng "khởi động lại ứng dụng của bạn" là một trong những việc nguy hiểm nhất mà một chương trình có thể được yêu cầu làm, bởi vì nó xảy ra đúng vào khoảnh khắc ứng dụng của bạn đang tự phá bỏ chính mình và có ít dư địa cho sai sót nhất.

Auto-update của GeekBye cần đến sáu bản phát hành trong bốn ngày — từ v1.5.14 đến v1.5.19 — để ổn định. Đây là câu chuyện về con bug tệ nhất trong giai đoạn đó, con bug mà chính chúng tôi gây ra khi cố gắng cẩn thận.

Sáu bản phát hành, và cái quan trọng nhất

Câu chuyện khởi đầu một cách bình thường. v1.5.14 sửa một con bug thuộc dạng lỗi đánh máy đáng xấu hổ: feed cập nhật trỏ đến một tên repo GitHub không tồn tại, nên trình cập nhật đang kiểm tra một trang 404. v1.5.15 thêm một nút "Check for Updates" thủ công và một thông báo lỗi thực sự. Rồi các bug quitAndInstall bắt đầu, và các bản phát hành đến dồn dập — bởi vì khi cơ chế cập nhật của bạn hỏng, bạn không thể ship bản sửa lỗi cho nó thông qua chính cơ chế cập nhật đó. Mỗi lần lặp là một ván cược cài đặt lại thủ công.

Cái quan trọng nhất là v1.5.18. Toàn bộ nội dung của nó là một commit duy nhất với một tiêu đề vẫn khiến tôi nhăn mặt: restore original quitAndInstall behavior to prevent unkillable app.

Làm thế nào mà "cẩn thận" lại làm hỏng ứng dụng

Đây là bối cảnh. Khi một bản cập nhật được tải về, quitAndInstall của Electron đáng lẽ phải đóng ứng dụng và thay thế bằng phiên bản mới. Trong một bản phát hành trước đó, ai đó — một cách hợp lý — lo lắng rằng việc tắt vô điều kiện là mạo hiểm. Lỡ như quá trình cài đặt bị lỗi thì sao? Chẳng phải sẽ an toàn hơn nếu chỉ tắt khi mọi thứ trông có vẻ ổn?

Vậy nên đoạn code mọc ra một lớp bảo vệ trông có vẻ hợp lý:

autoUpdater.quitAndInstall(false, true)
setTimeout(() => {
  if (this.updateDownloaded)
    app.quit() // only quit if the update is still "good"
  else console.log('Error detected — keeping app open')
}, 500)

Logic là: kích hoạt quá trình cài đặt, đợi nửa giây, và chỉ cưỡng chế lệnh app.quit() cuối cùng nếu cờ updateDownloaded vẫn còn là true — nếu không thì giữ ứng dụng mở để người dùng không bị mắc kẹt.

Cái bẫy chỉ cách đó một dòng, nằm trong trình xử lý lỗi. Trình xử lý đó đặt this.updateDownloaded = false. Vậy hãy hình dung một lần cài đặt thất bại: sự kiện error kích hoạt và xóa cờ. Nhưng quitAndInstall đã bắt đầu quá trình dọn dẹp — nó đã đóng các cửa sổ và gỡ bỏ các listener tắt của ứng dụng. Rồi bộ đếm 500ms thức dậy, kiểm tra cờ giờ đã là false, quyết định "phát hiện lỗi, giữ ứng dụng mở", và bỏ qua app.quit().

Giờ bạn có, đặc biệt là trên macOS, trạng thái tệ hại nhất có thể. macOS không tắt một ứng dụng chỉ vì cửa sổ cuối cùng của nó đã đóng — đó là hành vi window-all-closed mà mọi ứng dụng Mac đều dựa vào. Vậy nên tiến trình vẫn còn sống, nhưng nó không có cửa sổ, không có đường vào thanh menu, và các listener tắt của nó đã bị gỡ bỏ. Không có gì để nhấp vào. Cmd-Q không có gì để trò chuyện. Cách duy nhất để thoát ra là Force Quit từ Activity Monitor. Bước kiểm tra "an toàn" đã biến một bản cập nhật thất bại — một phiền toái có thể khôi phục — thành một xác sống mà bạn không thể kill.

Bản sửa lỗi: quá trình dọn dẹp phải vô điều kiện

Bản chỉnh sửa trong v1.5.18 gần như buồn tẻ một cách quyết liệt, và đó chính là điểm mấu chốt. Nó loại bỏ sự khôn khéo:

  1. Gỡ bỏ các listener window-all-closedbefore-quit có thể gây can thiệp.
  2. Hủy mọi cửa sổ — window.destroy(), không phải window.close(). Đóng có thể bị một trình xử lý phủ quyết; hủy thì không thể. Khi bạn đã quyết tâm tắt máy, bạn không hỏi han lịch sự.
  3. Gọi quitAndInstall.
  4. Gọi app.quit() vô điều kiện.

Không cờ, không bộ đếm, không "giữ nó mở để phòng hờ." Bởi vì sự thật về một con đường tắt máy là một lần tắt máy dở dang còn tệ hơn cả hai kết cục. Tắt hẳn thì ổn. Ở mở hẳn thì cũng ổn. Trạng thái duy nhất mà bạn không bao giờ được chạm tới là đã bị dọn dẹp nhưng vẫn đang chạy — và đó chính xác là trạng thái mà một lệnh tắt có điều kiện có thể khiến bạn mắc kẹt.

Hai bài học nữa mà cùng tuần đó dạy cho chúng tôi

Con bug không thể kill là tiêu đề chính, nhưng cuộc chạy nước rút sáu bản phát hành đã tôi luyện thêm hai thói quen đáng để học theo.

Lọc telemetry sự cố của bạn theo chữ ký chính xác, không theo từ khóa rộng. Giữa lúc chạy nước rút, chúng tôi phát hiện hệ thống báo cáo lỗi của mình được cấu hình để loại bỏ bất cứ thứ gì chứa những từ như permission, token, hay microphone — một nỗ lực cắt giảm nhiễu nhưng lại đang âm thầm nuốt chửng những sự cố thật tình cờ có nhắc đến những từ đó. Chúng tôi gỡ bỏ các bộ lọc bao trùm đó và thay bằng các chuỗi từ chối chính xác (thông báo cụ thể mà macOS phát ra khi một quyền bị từ chối) và các mã lỗi mạng tạm thời cụ thể như ERR_NETWORK_CHANGED. Giảm nhiễu và che giấu bug là cùng một núm vặn xoay theo hai hướng ngược nhau; nếu bạn lọc theo cảm tính, bạn sẽ lọc mất chính cái thứ bạn cần thấy.

Mọi con đường tự động đều cần một lối thoát thủ công. Auto-update về bản chất là nỗ lực tốt nhất có thể — mạng chập chờn, cài đặt thất bại. Vậy nên mỗi kiểu thất bại đều có một phương án dự phòng do con người điều khiển: nút "Check for Updates" thủ công, các lần thử lại theo lũy thừa lùi dần, một bộ đếm kiểm tra lại, và — dành riêng cho trường hợp nỗ lực thủ công của người dùng cũng thất bại — một thông báo bằng ngôn ngữ đơn giản "hãy xóa ứng dụng và cài lại từ trang web." Con đường tự động là sự tiện lợi; con đường thủ công là sự đảm bảo.

Điều đọng lại

  1. Một lớp bảo vệ quanh một hành động không thể đảo ngược còn nguy hiểm hơn chính hành động đó. Lệnh tắt có điều kiện cố ngăn một bản cập nhật tồi tắt ứng dụng, và thay vào đó lại tạo ra một trạng thái tệ hơn cả tắt lẫn không tắt. Các con đường tắt máy và cài đặt nên vô điều kiện và bất biến (idempotent) — không bao giờ bị chặn bởi một cờ có thể thay đổi mà một trình xử lý khác có thể lật ngược ngay dưới chân bạn.
  2. Trên macOS, "không có cửa sổ" không phải là "không có ứng dụng." Bất kỳ logic dọn dẹp nào cũng phải tính đến nền tảng nơi một tiến trình không cửa sổ vẫn tiếp tục chạy. Hãy kiểm tra con đường thất bại, trên chính hệ điều hành thật, chứ không chỉ con đường suôn sẻ.
  3. Tính năng mà bạn ship thông qua hệ thống cập nhật thì không thể kiểm tra thông qua hệ thống cập nhật. Sự bất đối xứng đó là lý do vì sao auto-update xứng đáng có được đoạn code đầy hoang tưởng, vô điều kiện, và được kiểm chứng thủ công kỹ lưỡng. Bạn chỉ được sửa nó theo cách dễ dàng sau khi nó đã hoạt động.

Đây là chương sớm nhất của công cuộc nâng cao độ tin cậy mà cuối cùng trở thành GeekBye v2. Để xem con đường đó dẫn tới đâu, hãy đọc what a version 2 actually takes (v2.0.0) và toàn bộ hành trình trong the anatomy of shipping software to perfection.

Bài Viết Liên Quan

Giải phẫu quá trình đưa phần mềm đến độ hoàn hảo: Cách đánh giá mã bắt được lỗi mà kiểm thử bỏ sót
Steven
Steven9 phút đọc

Giải phẫu quá trình đưa phần mềm đến độ hoàn hảo: Cách đánh giá mã bắt được lỗi mà kiểm thử bỏ sót

Xuyên suốt loạt bài GeekBye v2, cùng một điều cứ lặp đi lặp lại: một bản sửa lỗi vượt qua mọi bài kiểm thử trên máy của lập trình viên, rồi khâu đánh giá mã chứng minh rằng nó sẽ thất bại với gần như tất cả mọi người. Đây là quy trình đằng sau chín bản phát hành — cửa kiểm soát đánh giá, những phát hiện bắt lỗi ngay từ đầu, và kỷ luật kiểm thử trước khi phát hành đã biến "nó chạy được trên máy tôi" thành "nó chạy được".

Kỹ thuật
Quy trình
Đánh giá mã
Ngày ứng dụng của chúng tôi tự DDoS chính mình
Steven
Steven7 phút đọc

Ngày ứng dụng của chúng tôi tự DDoS chính mình

Một đống bản tải lên còn tồn đọng, được xả ra cùng một lúc lúc khởi động, đã biến mỗi client GeekBye thành một đợt tấn công từ chối dịch vụ nhỏ nhắm vào chính máy chủ của chúng tôi. Cách sửa — và cái thang connection-liveness mà nó buộc chúng tôi phải dựng — là một trong những thứ hữu ích nhất mà v2 đã dạy chúng tôi.

Độ tin cậy
Mạng
Kỹ thuật
Một phiên bản 2 thực sự đòi hỏi những gì: 206 commit cho những trạng thái trung thực
Steven
Steven9 phút đọc

Một phiên bản 2 thực sự đòi hỏi những gì: 206 commit cho những trạng thái trung thực

GeekBye v2 không phải là một bản phát hành tính năng. Nó là 206 commit nhắm tới một ý tưởng duy nhất: ứng dụng không bao giờ được nói dối về trạng thái của chính nó. Đây là cái giá phải trả — bao gồm cả lỗi một dòng trong tệp lock suýt khiến chúng tôi không thể phát hành bất cứ thứ gì.

Độ tin cậy
Kỹ thuật
Phát hành