Mời bạn thưởng thức Newsletter #137.
How to name things
Koleman Nix cho rằng đặt tên là một trong những kỹ năng nền tảng nhất của kỹ sư phần mềm, vì ngôn ngữ rõ ràng gần như luôn đi kèm tư duy rõ ràng. Mỗi lần đặt tên biến, hàm, endpoint hay cột database, ta đang giao tiếp với đồng nghiệp và cả LLM. Model thường làm rất tốt trên codebase mới, gọn gàng, nhưng dần trở nên ì ạch khi tên gọi và thuật ngữ trong code bị “slop-fried”, nên muốn làm việc bền vững với model thì phải liên tục tinh giản thuật ngữ. DRY hiểu đúng là mỗi sự thật chỉ nằm ở một chỗ, nhưng hai giá trị bằng nhau chưa chắc là cùng một sự thật: độ dài tên tối đa và độ sâu hàng đợi cùng bằng 64 vẫn nên là hai hằng số riêng. Tác giả phản đối các quy tắc đặt tên cứng nhắc, vì đó là cách né tránh suy nghĩ. Điều cần nhất quán là quá trình đặt tên: hiểu bản chất của thứ đang đặt tên và cân nhắc tính đối xứng như src và dst.
Đặt tên tốt là sự thấu cảm trí tuệ, còn đặt tên theo nhu cầu nhất thời là ích kỷ. Cột created_at hợp với dữ liệu nội bộ, nhưng một giấy khai sinh được upload năm 2023 lại cần uploaded_at và record_date riêng. Hàm mergeTargetValuesIntoSourceTemplate thực ra chỉ là phép hợp map ++, nhưng cái tên theo nhu cầu đã che mất tính tổng quát. Một trường mơ hồ như documentDate có thể bị hiểu theo hai nghĩa khác nhau ở hai nơi trong hệ thống và gây bug. Kết luận: hãy đặt tên theo bản chất chứ không theo nhu cầu.
Postgres Locks Explained
Postgres Locks Explained là một trang tài liệu tương tác về lock trong Postgres, “từ người mới tới chuyên gia”, do Brian (@TheOtherBrian1), một customer reliability engineer chuyên về quản trị và observability Postgres, xây dựng. Đây là bộ tài liệu tác giả ước gì đã có khi mới học về lock. Phần Concept giải thích bằng animation vì sao cần lock: nếu một câu DELETE đang xóa dở một dòng mà SELECT chen vào đọc, người đọc có thể nhận về dữ liệu bị xóa một nửa. Phần Locks by Example hướng dẫn tự chạy demo với hai session, chẳng hạn một ALTER TABLE trong transaction chưa commit giữ lock ACCESS EXCLUSIVE khiến cả SELECT ở session khác bị treo, hay lock FOR UPDATE giữ cho ràng buộc UNIQUE được bảo toàn khi hai câu UPDATE cùng ghi một giá trị. Lock Blocking Graph là công cụ tương tác cho biết thao tác SQL nào chặn thao tác nào, từ SELECT, CREATE INDEX CONCURRENTLY tới VACUUM FULL.
Phần Troubleshooting đúc kết từ việc xem hàng nghìn database production mỗi năm: nghẽn do lock hiếm hơn query chưa tối ưu hay quản lý connection kém, nhưng chính vì ít gặp nên khi xảy ra, ít developer biết cách phát hiện và xử lý. Các vấn đề được phân tích gồm deadlock, lỗi hết bộ nhớ lock cần tăng max_locks_per_transaction, hàng đợi lock, và trường hợp bảng bị chặn hoàn toàn. Tác giả khuyên bật log_lock_waits để ghi lại các query chờ lock lâu hơn deadlock_timeout. Phần Monitoring chỉ cách kết hợp pg_locks, pg_stat_activity và pg_blocking_pids() để tìm query bị chặn và thủ phạm, đồng thời giới thiệu các truy vấn lock tree mà Datadog và pganalyze sử dụng.
A Deep (and Fuzzy) Dive Into Search
Andrey Abramov, CTO của SereneDB, viết lại bài phân tích về fuzzy search từ sáu năm trước, khi team lần đầu ra mắt tính năng này trong engine IResearch, nay là lõi tìm kiếm của SereneDB. Fuzzy search là tên gọi chung cho các thuật toán so khớp gần đúng, mỗi thuật toán định nghĩa một thước đo độ tương đồng riêng. Thước đo đầu tiên là khoảng cách Levenshtein, số thao tác chèn, xóa, thay ký tự tối thiểu để biến chuỗi này thành chuỗi kia. Thuật toán quy hoạch động Wagner–Fischer tốn thời gian bậc hai cho mỗi cặp từ, quá chậm với từ điển lớn. Giải pháp là Levenshtein automaton của Schulz và Mihov (2002): với một từ và khoảng cách tối đa, dựng trong thời gian tuyến tính một automaton chỉ chấp nhận các chuỗi nằm trong khoảng cách đó, rồi giao với trie hoặc FST của từ điển để cắt bỏ các nhánh không thể khớp. Vì bảng chuyển trạng thái phình rất nhanh, SereneDB giới hạn khoảng cách tối đa là 4 cho Levenshtein và 3 cho Damerau–Levenshtein, biến thể coi việc đổi chỗ hai ký tự liền kề là một thao tác.
Levenshtein thiên vị theo độ dài từ và không xử lý được từ thừa hay thiếu trong cụm từ, nên SereneDB bổ sung độ tương đồng n-gram dựa trên công trình của Kondrak, tính longest common subsequence trên n-gram thay vì ký tự. Ngưỡng chỉ chuẩn hóa theo query, nên query ngắn vẫn tìm được trường dài. Trong SQL, ts_levenshtein tự chọn khoảng cách theo độ dài query và hỗ trợ sửa lỗi chính tả, còn ts_ngram mặc định ngưỡng 0,7. Kết luận là không phương pháp nào thắng tuyệt đối, và nhiều hệ thống thực tế dùng cả hai.
email encryption
J. B. Crawford, tác giả newsletter Computers Are Bad, kể lại lịch sử dài của việc mã hóa email. Email ra đời trong bối cảnh tin cậy lẫn nhau từ thập niên 60, nên đến nay vẫn gần như không có cơ chế bảo mật hay toàn vẹn dữ liệu sẵn có. Bộ giao thức OSI từng có X.400 nhưng thua email internet và chỉ còn sống trong những môi trường được quản lý tập trung như quân đội hay hàng không. MIME, mà tác giả gọi là “email 2.0”, giải quyết vấn đề nội dung 8-bit và mở đường cho attachment. PGP của Phil Zimmermann (1991), sau là GnuPG, phân phối khóa qua web of trust, nhưng mô hình này thất bại vì khó mở rộng, khó dùng. S/MIME (1996) dựa trên PKI với certificate do CA ký, phổ biến trong doanh nghiệp và chính phủ nhờ Microsoft Exchange và Active Directory, nhưng rất khó dùng ngoài một tổ chức.
Ngày nay, phần lớn email thực ra được mã hóa khi truyền giữa các server, chủ yếu qua STARTTLS. Điểm yếu là STARTTLS mang tính cơ hội: kẻ tấn công đứng giữa có thể xóa lời đề nghị STARTTLS để ép kết nối về plaintext. Các biện pháp bổ sung gồm DANE dựa trên DNSSEC, MTA-STS công bố yêu cầu TLS qua HTTPS, và việc ghim yêu cầu mã hóa với các đối tác lớn. Kết luận của tác giả là mã hóa đầu cuối luôn chỉ là lớp vá gắn thêm cho một hệ thống không được thiết kế cho nó, PGP và S/MIME vẫn không phù hợp với đa số người dùng, và bảo mật thực tế đến từ mã hóa tầng truyền tải do nhà cung cấp cấu hình.
A shell exclamation mark is not for yelling. Be lazy.
Filip Roséen nhắc lại một tính năng có từ cuối thập niên 70 nhưng ít người dùng: event designator trong bash, zsh, csh và tcsh. Lập trình viên được dạy áp dụng DRY ở khắp nơi, trừ trên command line, nơi ta vẫn bấm mũi tên lên liên tục để lặp lại lệnh cũ. Ví dụ quen thuộc nhất là sudo !! để chạy lại lệnh trước với quyền root, hay mkdir /var/mnt/cache rồi cd !$ để vào thư mục vừa tạo. Cú pháp luôn theo mẫu ![event][:word][:modifier]. Phần event chọn dòng lệnh: !! là lệnh trước, !-2 là hai lệnh trước, !ssh là lệnh gần nhất bắt đầu bằng “ssh”, !?needle? là lệnh gần nhất chứa “needle”, còn ^ssh^scp^ thay chữ trong lệnh trước. Phần word chọn đối số: !:0 là lệnh, !:1 là đối số đầu, !$ là đối số cuối, !* là tất cả đối số. Phần modifier biến đổi đối số: :h lấy thư mục như dirname, :t lấy tên file như basename, :r bỏ phần mở rộng, :s/a/b/ thay chuỗi, và :p chỉ in ra mà không chạy.
Với shell POSIX không hỗ trợ !, lệnh fc cho phép mở lệnh cũ trong editor để sửa, hoặc dùng fc -e 'sed -i s/prod-01/test-01/g' -3 -1 để chạy lại ba lệnh gần nhất trên một server khác. Tác giả đề xuất nhóm thiết yếu gồm !!, !needle, !?needle?, !$, !:0, !$:h và !$:t. Ai chưa tự tin có thể bật histverify trong bash hay HIST_VERIFY trong zsh để xem lệnh được mở rộng trước khi chạy. Lịch sử shell lưu lệnh đã mở rộng chứ không lưu designator, và nếu ! gây phiền thì có thể tắt bằng set +H hoặc đổi sang ký tự khác qua histchars.
A coding agent is six functions in a trenchcoat
Hadley Wickham giải thích điều gì làm nên một coding agent như Claude Code, Cursor hay Codex. Agent là một harness kèm các tool để đọc và ghi, còn coding agent là agent có những tool cho phép LLM khám phá và sửa codebase giống như con người. Hầu hết coding agent cung cấp sáu tool: đọc file, ghi file, sửa file, liệt kê file, tìm kiếm và chạy lệnh shell, kèm một system prompt dài về các thực hành tốt. Để chứng minh, tác giả viết một coding agent tí hon bằng R với thư viện ellmer, bắt đầu chỉ với ba tool read_file, write_file và run_command, mỗi hàm chỉ vài dòng. Đăng ký ba hàm này kèm mô tả rõ ràng và một prompt cơ bản là đã có một agent hoàn chỉnh có thể tìm hàm parse ngày tháng rồi viết unit test cho nó. Tool shell là “thẻ thoát hiểm” vì làm được mọi thứ, từ ls, grep tới git, nhưng cũng rất nguy hiểm.
Phụ thuộc hết vào shell có nhiều nhược điểm, nhất là rất khó biết một lệnh shell an toàn hay không. Vì thế tác giả thêm tool liệt kê và tìm kiếm riêng, rồi làm chúng an toàn hơn bằng hàm safe_path kiểm tra đường dẫn sau khi chuẩn hóa vẫn nằm trong thư mục dự án. list.files() mặc định bỏ qua dotfile nên .Renviron chứa API key cũng không bị lộ. Cuối cùng, tool edit_file thay đúng một đoạn văn bản bằng đoạn khác, giúp model chỉ viết vài dòng thay đổi thay vì chép lại cả file 500 dòng, vừa rẻ vừa an toàn hơn vì một lần sửa sai sẽ báo lỗi rõ ràng thay vì âm thầm làm hỏng file.
How well do agents use test and verification techniques?
Dan Luu thử nghiệm xem việc chỉ dặn coding agent dùng một kỹ thuật kiểm thử hay thư viện cụ thể có giúp code đúng hơn không. Bài toán là viết Zstd bằng Rust, dùng Codex với GPT-5.6 Sol ở mức effort medium và xhigh, qua 26 điều kiện prompt như mặc định, TDD, “Make no mistakes”, audit, fuzzing, differential testing, property-based testing, mutation testing và nhiều công cụ formal như TLA+, Kani, Verus, Lean 4. Tác giả còn thử bốn skill. Mỗi điều kiện chạy khoảng 160 lần, chấm theo tỷ lệ vượt toàn bộ hidden test so với chi phí.
Kết quả là không điều kiện nào vượt trội rõ rệt, và điều kiện mặc định đạt điểm khá cao. Agent thường dùng kỹ thuật một cách hời hợt, viết test thông thường bên trong framework được nhắc tới. Với Verus, agent chứng minh các tính chất trừu tượng hay vô nghĩa thay vì code thật; 159/160 lần viết model TLA+ nhưng không lần nào làm thay đổi code Rust; 135/160 lần “differential testing” nhưng không lần nào có hai bản hiện thực độc lập. TDD cho kết quả kém, thường bỏ sót trường hợp jump table bốn stream. Fuzzing chủ yếu sinh byte ngẫu nhiên chạm cùng một nhánh từ chối, dù 10 lần dùng input có cấu trúc thì một nửa tìm ra bug thật. Skill Hegel không cải thiện độ đúng mà còn tăng chi phí 26–41% do dung lượng lớn. Agent nhận ra được vùng rủi ro khi được hỏi nhưng không test kỹ chúng. Kết luận: chỉ nêu tên kỹ thuật là không đủ, cần hướng dẫn cụ thể, dựng sẵn cấu trúc test hợp lý rồi điều chỉnh dần theo những gì agent làm ra.
How one connection kills a database
Simeon Griggs của PlanetScale mở đầu bằng một câu hỏi phỏng vấn: mọi query đều đang thất bại, hãy tìm ra nguyên nhân. Ví dụ được nhắc lại là một sự cố ở GitHub: một thay đổi schema MySQL cần lock độc quyền trên bảng, nhưng một session khác đã chạm vào bảng mà không commit, nên transaction đang mở này chặn thay đổi schema, rồi mọi query sau đó lại xếp hàng phía sau thay đổi schema đang chờ. Không có deadlock, không có timeout, và tình trạng kéo dài cho tới khi có người tìm và kill đúng connection ở đầu chuỗi. Tác giả tái hiện kịch bản này trên Postgres: một connection của ứng dụng chạy BEGIN và SELECT trên bảng orders rồi gặp exception chưa được xử lý nên không bao giờ COMMIT hay ROLLBACK. Câu SELECT chỉ giữ lock ACCESS SHARE, nhưng migration ALTER TABLE orders ADD COLUMN cần ACCESS EXCLUSIVE nên phải chờ, và Postgres không cho query mới vượt lên trước migration đang chờ, khiến toàn bộ request vào bảng thất bại hoặc timeout.
Tất cả bắt nguồn từ một exception chưa được xử lý, và có thể phòng tránh bằng idle_in_transaction_session_timeout, vốn bị tắt mặc định. Debug thủ công khá vụng về, và vô dụng khi slot connection đã cạn. Vì vậy PlanetScale ra mắt tab Connections dùng một connection quản trị dự phòng, hiển thị process, trạng thái, thời gian và số query bị chặn, kèm ba mức kill: hủy query, kết thúc transaction, hoặc kết thúc connection. Trong ví dụ, SELECT đã chạy xong nên hủy query không giải phóng được lock. CLI có lệnh pscale branch connections top hiển thị cây blocker và cập nhật mỗi giây, cùng lệnh show --format json cho script và agent.
TODOs Considered Harmful
Andrew Oliver cảnh báo rằng trong thời đại coding agent hoạt động với ngày càng ít sự giám sát của con người, các comment TODO đang trở nên nguy hiểm. Chúng thường được viết vội, thiếu ngữ cảnh, và giờ là những quả bom hẹn giờ chờ một agent vô tình gặp phải rồi hành động theo. Trong năm qua, developer đã chuyển từ việc dùng agent như một StackOverflow theo yêu cầu sang giao cho chúng những nhiệm vụ lớn hơn, đôi khi với kỳ vọng agent tự tìm và sửa lỗi. TODO, vốn được coi là nợ kỹ thuật, trở thành mục tiêu dễ thấy cho các đợt dọn dẹp tự động: nếu developer đã để lại ghi chú về thứ cần sửa, sao không để agent làm luôn?
Vấn đề là TODO hiếm khi được soi xét kỹ như code thực thi. Chúng có thể được viết nhanh, thiếu chi tiết, hoặc trở nên lỗi thời, thậm chí sai, sau những thay đổi code khác, và chắc chắn không bao giờ được kiểm chứng bằng unit test, mutation test hay integration test. Agent có rất ít khả năng đánh giá tính đúng đắn của một TODO, và vì code là một phần ngữ cảnh, chúng có thể coi TODO như sự thật rồi biến một TODO sai thành code thật, gây bug hoặc sự cố. Tác giả khẳng định đã tận mắt thấy điều này xảy ra trên môi trường production. Lời khuyên gồm: hạn chế TODO trong code mới và khi review, quét định kỳ các TODO đã cũ, sửa ngay nếu đủ quan trọng để ghi lại, ghi rõ lý do và ngữ cảnh nếu buộc phải giữ, hoặc chuyển chúng thành story có tiêu chí chấp nhận để theo dõi và ưu tiên.
git worktree gotchas
Olaf Alders nhận thấy git worktree, vốn từng bị coi là công cụ ngách, giờ đã phổ biến, nhưng vẫn có những cái bẫy cần biết. Mọi checkout git đều là một worktree: thư mục clone ban đầu là main working tree, còn các worktree tạo thêm là linked worktree. Tất cả các bẫy đều bắt nguồn từ một sự thật: trong linked worktree, .git không phải thư mục mà là một file trỏ tới thư mục git riêng của worktree đó. Vì thế, đoạn code tự đi ngược lên tìm thư mục .git để xác định gốc repository sẽ vượt qua đỉnh của linked worktree, và nếu linked worktree nằm trong main working tree, nó sẽ dừng ở main working tree, nơi không chứa thay đổi của bạn. Tác giả giữ linked worktree ở một đường dẫn hoàn toàn khác, và khuyên dùng git rev-parse --show-toplevel để tìm gốc worktree hiện tại. Để tìm main working tree, chẳng hạn khi cần file .env bị gitignore, dùng git rev-parse --git-common-dir rồi lùi lên một cấp bằng dirname.
Bẫy thứ ba liên quan tới git hook khi linked worktree chạy trong sandbox như nono, Docker, Landlock hay Seatbelt. Hook là tài nguyên dùng chung nằm trong .git/hooks của main working tree, mà sandbox lại chặn truy cập vào đó. Giải pháp là đưa hook vào thư mục .githooks được commit trong repository và cấu hình git config core.hooksPath .githooks bằng đường dẫn tương đối. Vì git chuyển vào gốc của từng worktree trước khi chạy hook, đường dẫn này luôn trỏ vào bên trong worktree và sandbox, và chỉ cần cấu hình một lần cho mọi worktree. Thông điệp cần nhớ: .git có thể là thư mục hoặc con trỏ, tùy bạn đang đứng ở đâu.