# Rust: Tăng Năng Suất Bất Ngờ

## TL;DR

Backend của Lubeno được viết hoàn toàn bằng Rust và đã phát triển đến quy mô không thể nào tôi nắm vững tất cả các phần của mã nguồn trong đầu.

Dựa trên kinh nghiệm của tôi, các dự án thường chậm lại đáng kể ở giai đoạn này. Chỉ cần đảm bảo những thay đổi của bạn không gây ra hậu quả bất ngờ cũng đã trở nên rất khó khăn.

Tôi đã nhận thấy rằng các cam kết an toàn mạnh mẽ của Rust mang lại cho tôi sự tự tin nhiều hơn khi chạm vào mã nguồn. Với sự tự tin đó, tôi sẵn sàng refactor thậm chí cả những phần quan trọng của ứng dụng, điều này có tác động rất tích cực đến năng suất và khả năng bảo trì lâu dài của tôi.

Hôm nay Rust lại một lần nữa cứu tôi!

## Vấn đề về Mutex và Giao tiếp giữa các luồng

Gần đây, tôi gặp một vấn đề khiến tôi suy nghĩ và cuối cùng truyền cảm hứng để tôi viết bài viết này. Tôi cần bọc một cấu trúc vào trong mutex, vì nó đang được truy cập đồng thời. Để truy cập cấu trúc nội bộ, bạn cần phải có khóa (lock) trên mutex đầu tiên.

rust
let lock = mutex.lock();
// … Dữ liệu đã khóa được sử dụng để tạo một commit …
db.insert_commit(commit).await;
“`

Thay đổi này trông hoàn toàn ổn với tôi và rust-analyzer cũng đồng ý. Không có lỗi nào được hiển thị trong tệp đó. Nhưng đột nhiên một tệp khác trong trình soạn thảo của tôi sáng màu đỏ, cho thấy lỗi biên dịch, định nghĩa router. Điều này không hề có ý nghĩa đối với tôi, làm thế nào lock của tôi lại ảnh hưởng đến handler mà router sẽ lấy?

“`rust
.route(“/api/git/post-receive”, post(git::post_receive))
^^^^^^^^^^^^^^^^^
error: future cannot be sent between threads safely
help: within ‘impl Future>’, the trait ‘Send’ is not implemented for “MutexGuard<'_, GitInternal>”
“`

Tôi đã mất nhiều thời gian hơn mức tôi muốn thừa nhận, để tìm ra đang xảy ra chuyện gì. Để tôi giải thích cho bạn!

Khi một kết nối HTTP mới đến, framework web mà chúng tôi đang sử dụng sẽ tạo một tác vụ (task) async mới cho nó. Các tác vụ async được thực thi trên một lập lịch trình (scheduler) lấy cắp công việc (work-stealing). Điều này có nghĩa là nếu một luồng hoàn thành tất cả công việc, nó sẽ “lấy cắp” các tác vụ từ các luồng khác để cân bằng khối lượng công việc. Điều này chỉ có thể xảy ra tại các điểm ‘.await’ trong Rust.

Có một quy tắc quan trọng khác, nếu mutex được khóa trên một luồng, nó cần được giải phóng trên cùng một luồng, nếu không chúng ta sẽ có hành vi không xác định.

Bây giờ, Rust theo dõi tất cả các vòng đời (lifetimes) và biết rằng lock tồn tại đủ lâu và vượt qua điểm ‘.await’. Điều này có nghĩa là việc giải phóng lock có thể xảy ra trên một luồng khác và điều đó không được phép, vì nó có thể dẫn đến hành vi không xác định.

Giải pháp rất đơn giản, chỉ cần giải phóng lock trước câu lệnh ‘.await’.

Những lỗi như thế này tồi tệ nhất! Gần như không thể bắt chúng trong quá trình phát triển, vì hệ thống không bao giờ có đủ tải để buộc lập lịch trình di chuyển việc thực thi đến một luồng khác. Vì vậy, bạn kết thúc với một trong những lỗi “không thể tái tạo, thất bại đôi khi, nhưng không bao giờ cho bạn”.

Thật tuyệt vời khi trình biên dịch Rust có thể phát hiện ra điều như vậy. Và những phần dường như không liên quan của ngôn ngữ, như mutex, vòng đời và hoạt động async, tạo thành một hệ thống nhất quán như vậy.

## TypeScript thì đáng sợ hơn

Ngược lại, một lỗi async gần đây trong mã nguồn TypeScript của chúng tôi đã không bị phát hiện trong một thời gian dài sau khi được triển khai lên sản xuất. Đây là thủ phạm:

“`javascript
// Người dùng đăng nhập thành công!
if (redirect) {
window.location.href = redirect;
}

let content = await response.json();
if (content.onboardingDone) {
window.location.href = “/dashboard”;
} else {
window.location.href = “/onboarding”;
}
“`

Rất đơn giản. Khi đăng nhập, xem xem có chuyển hướng không. Nếu có, chuyển hướng đến trang cụ thể. Nếu không, đi đến bảng điều khiển hoặc trang onboarding. Gán một giá trị cho ‘window.location.href’ sẽ chuyển hướng trình duyệt của bạn đến một vị trí.

Tôi tin rằng tôi đã kiểm tra nó và nó đã hoạt động. Nhưng đột nhiên nó không hoạt động nữa. Nó đã từng hoạt động chưa? Điều gì đang xảy ra ở đây? Chúng tôi luôn bị chuyển hướng đến bảng điều khiển, ngay cả khi có chuyển hướng.

Ở đây có một điều kiện cạnh tranh về lập lịch trình. Gán giá trị cho ‘window.location.href’ không chuyển hướng bạn ngay lập tức, như tôi nghĩ sẽ làm. Nó chỉ đặt giá trị và lên lịch chuyển hướng sớm nhất có thể. Nhưng mã không dừng thực thi! Điều này có nghĩa là việc gán tiếp theo có thể thực thi trước khi trình duyệt bắt đầu chuyển hướng, chuyển hướng bạn đến vị trí sai. Tôi đã mất mãi mãi để tìm ra trường hợp đó. Giải pháp đơn giản là chỉ cần thêm một câu lệnh return vào khối if và không bao giờ để nó đến phần còn lại.

“`javascript
if (redirect) {
window.location.href = redirect;
return;
}
“`

Tôi cảm thấy cả hai vấn đề, vấn đề của Rust và TypeScript, đều giống nhau. Chúng đều liên quan đến lập lịch async và đều cho thấy một số hành vi không xác định rất không rõ ràng. Nhưng trình kiểm tra kiểu của Rust hữu ích hơn nhiều, nó ngăn chặn lỗi bao giờ biên dịch được. Trình biên dịch TypeScript không theo dõi vòng đời hoặc có bất kỳ quy tắc mượn nào và đơn giản là không thể bắt được loại vấn đề này.

## Refactoring không sợ hãi

Rust thường được giới thiệu là một ngôn ngữ tuyệt vời cho lập trình hệ thống, nhưng nó không phải là lựa chọn đầu tiên khi nói đến ứng dụng web. Python, Ruby và JavaScript/Node.js luôn được coi là “năng suất” hơn cho phát triển web. Tôi nghĩ rằng điều này là đúng nếu bạn mới bắt đầu! Bạn nhận được rất nhiều thứ có sẵn với những ngôn ngữ này và tiến độ ban đầu rất nhanh.

Nhưng một khi dự án đạt đến một quy mô nhất định, mọi thứ đều chậm lại. Có quá nhiều sự liên kết lỏng lẻo giữa các phần của mã nguồn khiến việc thay đổi bất cứ thứ gì trở nên rất khó khăn.

Chúng tôi đều đã ở đó. Bạn thay đổi một thứ và mọi thứ hoạt động tuyệt vời, nhưng hai ngày sau đó bạn nhận được một thông báo rằng thay đổi của bạn đã làm hỏng một trang khác (hoàn toàn không liên quan). Sau lần thứ 3 điều này xảy ra, sự sẵn sàng của bạn khi chạm vào mã nguồn giảm mạnh.

Với Rust, tôi chỉ phải lo lắng ít hơn nhiều, và điều này cho phép tôi thử nghiệm nhiều thứ hơn. Tôi có cảm giác năng suất của tôi thậm chí đã tăng lên khi mã nguồn phát triển. Có nhiều mã hơn mà tôi có thể xây dựng, tái sử dụng và thay đổi mà không phải lo lắng rằng tôi sẽ vô tình phá vỡ thứ hiện có.

Rust quá giỏi trong việc nói cho bạn biết “Vâng, thay đổi bạn đang làm đang ảnh hưởng đến một phần khác của dự án mà bạn có thể không suy nghĩ đến tất cả, vì bạn đang ở sâu sáu lần gọi hàm và deadline đang đến nhanh chóng, nhưng đây chính xác lý do tại sao điều này có thể gây ra vấn đề”.

## Vậy thì kiểm thử thì sao?

Tôi nghĩ rằng kiểm thử rất tuyệt vời! Chúng là một công cụ rất mạnh mẽ nếu bạn đang thực hiện một refactoring lớn và cần giúp đỡ trong việc bắt các lỗi hồi quy. Nhưng chúng không bắt buộc bởi trình biên dịch để mã chạy. Điều này có nghĩa là bạn có thể đơn giản quyết định không thêm kiểm thử.

Một số ngày chỉ căng thẳng hơn những ngày khác, rất ít thời gian và những thứ cần được hoàn thành. Nhưng với kiểm thử, có thêm gánh nặng tinh thần này. Tôi cần quyết định mức độ trừu tượng nào là đúng. Tôi đang kiểm tra hành vi hay chi tiết triển khai? Kiểm thử này thực sự sẽ ngăn chặn bất kỳ lỗi nào trong tương lai? Việc đưa ra tất cả những quyết định này rất mệt mỏi và dễ mắc lỗi.

Rust đôi khi có thể thách thức để học và viết, nhưng điều tốt đẹp với Rust là nó gánh gánh nặng quyết định khỏi tôi. Các quyết định đã được đưa ra bởi những người thông minh hơn tôi nhiều, những người đã làm việc trên các mã nguồn lớn và mã hóa tất cả các lỗi phổ biến vào trình biên dịch.

Chắc chắn, một số thuộc tính của ứng dụng không thể là một phần của hệ thống kiểu. Trong trường hợp đó, kiểm thử thì tuyệt vời!

## Bouns: Zig cũng đáng sợ!

Zig thường được so sánh với Rust; cả hai đều nhắm đến việc trở thành ngôn ngữ lập trình hệ thống. Tôi nghĩ rằng Zig rất tuyệt, và ngôn ngữ này chỉ đơn giản là tạo ra niềm vui nerd trong tôi. Nhưng sau đó tôi nhớ lại rằng nó cũng đáng sợ. Hãy chỉ nhìn vào một ví dụ xử lý lỗi đơn giản.

“`zig
const std = @import(“std”);

const FileError = error{
AccessDenied,
};

fn doSomethingThatFails() FileError!void {
return FileError.AccessDenied;
}

pub fn main() !void {
doSomethingThatFails() catch |err| {
if (err == error.AccessDenid) {
std.debug.print(“Access was denied!\n”, .{});
} else {
std.debug.print(“Unexpected error!\n”, .{});
}
};
}
“`

Chúng ta có một hàm tên là ‘doSomethingThatFails’ luôn thất bại với giá trị lỗi của ‘FileError.AccessDenied’, sau đó chúng ta bắt lỗi và in ra rằng truy cập bị từ chối.

Ngoại trừ là chúng ta không làm như vậy. Có một lỗi chính tả trong logic xử lý lỗi ‘AccessDenid != AccessDenied’. Mã sẽ biên dịch hoàn hảo. Trình biên dịch Zig sẽ tạo một số mới cho mỗi ‘error.*’ độc đáo và không quan tâm đến các loại bạn đang so sánh. Chỉ là số.

Tuy nhiên, nếu bạn sử dụng câu lệnh ‘switch’ thay vì ‘if’, đột nhiên trình biên dịch Zig nói “Ôi điều này rõ ràng là sai! Lỗi trả về không bao giờ có thể có giá trị này vì nó không nằm trong ‘FileError'”, và từ chối biên dịch mã. Nó có khả năng phát hiện lỗi, nó chỉ đơn giản là chọn không quan tâm. Nếu trông giống như một số, câu lệnh ‘if’ cũng có thể so sánh nó như một số.

Những quyết định thiết kế nhỏ trong ngôn ngữ này hoàn toàn trái ngược với Rust. Và đối với một người thường xuyên gõ nhầm tên, điều đó có thể đáng sợ.