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

Một cú rớt kết nối không nên làm sập cả ứng dụng — nhưng của chúng tôi thì có
Steven
Steven8 phút đọc

Một cú rớt kết nối không nên làm sập cả ứng dụng — nhưng của chúng tôi thì có

Khi backend của chúng tôi rớt mạng giữa cuộc họp, nó không chỉ tạm dừng việc chép lời — nó làm sập toàn bộ ứng dụng. Nguyên nhân là một sự kiện đơn lẻ không được xử lý, và bản vá chỉ mười dòng. Đây là cụm bản phát hành đã khiến GeekBye giữ được đăng nhập và giữ được kết nối xuyên qua những thứ từng giết chết nó.

Kỹ thuật
Độ tin cậy
Electron
Sự Im Lặng Vốn Chịu Tải
Steven
Steven10 phút đọc

Sự Im Lặng Vốn Chịu Tải

Hai bản phát hành cuối cùng của GeekBye v1 đều xoay quanh cùng một sự thật khó chịu: phiên âm thời gian thực trên một mạng thật không phải là không mất mát, và nước đi trung thực là thôi giả vờ rằng nó như vậy. v1.8.20 giữ một bản sao của mỗi audio chunk trên đĩa trước khi bỏ nó trong một lần kết nối lại, và bắt đầu đánh dấu các khoảng trống trong transcript ra thành tiếng. v1.9.0 thôi gửi im lặng để tiết kiệm bandwidth — rồi phát hiện ra im lặng chính là tín hiệu chính xác mà bộ phiên âm dùng để biết một câu đã kết thúc. Hai bản phát hành về cái giá của việc vứt đồ đi.

Kỹ thuật
Audio
Độ tin cậy
In Một Cuộc Họp Ra PDF Mà Không Có Thư Viện PDF
Steven
Steven11 phút đọc

In Một Cuộc Họp Ra PDF Mà Không Có Thư Viện PDF

GeekBye xuất một cuộc họp ra PDF, mà chẳng có một thư viện PDF nào ở bất cứ đâu trong code. Nó render HTML trong một cửa sổ trình duyệt vô hình rồi in nó ra. Lựa chọn đó chính là cả câu chuyện: nó làm cho tính năng dễ dựng và trao cho nó mọi kiểu hỏng mà một trình duyệt thật có — một cú lóe trắng, một giới hạn độ dài URL, và một chỗ ngắt trang cắt đôi các screenshot. Cách sửa cho con bug xấu xí nhất là một dòng CSS.

Kỹ thuật
Electron
Desktop