Quần Cam

ExUnit capture log và Erlang IO system

Xin chào lại các bạn, sau gần nửa năm không viết bài mới, Quần Cam xin được bật nắp cây bút với loạt bài về đề tài đã cũ: Elixir và Erlang. Mở hàng bằng bài viết về Erlang IO system này.

image

Budapest Chain Bridge by Bergadder on Pixabay

ExUnit

ExUnit là một unit testing framework được ship chung với ngôn ngữ Elixir. Nó hỗ trợ khá nhiều tính năng cần thiết trong kiểm thử như: assertions, doc tests, formatters, filters, IO capturing, test case templating … Trên tất cả, tính năng nổi bật nhất của framework này là khả năng chạy đồng thời nhiều test case. Test case trong một module được khai báo async: true sẽ được chạy cùng lúc với nhau khi máy tính của bạn có nhiều nhân CPU. Nhờ đó, test suite chạy nhanh hơn, nâng cao hiệu suất làm việc của lập trình viên.

defmodule MyTestCase do
  use ExUnit.Case, async: true

  test "1", do: assert(1 == 1)

  test "2", do: refute(2 == 1)
end

Capture Log

Đôi lúc khi lập trình, bạn có nhu cầu muốn bắt lại log để phục vụ cho mục đích kiểm thử. Để làm điều này, ExUnit hỗ trợ hàm tiện ích ExUnit.CaptureLog.capture_log/1, với đầu vào là một anonymous function. Hàm này sử dụng CaptureIO ở bên dưới, bắt hết tất cả những gì xuất ra STDOUT và trả chúng về cho bạn dưới dạng string.

test "logs when some thing happens" do
  log =
    ExUnit.CaptureLog.capture_log(fn ->
      MyModule.my_function()
    end)

  assert log =~ "Logs from MyModule.my_function()"
end

Tính năng này không có gì mới. Hầu hết các ngôn ngữ hiện hành đều hỗ trợ nó dưới dạng này hay dạng khác. Ví dụ như thư viện kiểm thử nổi tiếng RSpec của Ruby với matcher output. Matcher này hoạt động bằng cách tạm thời ghi đè vào biến $stdout của máy ảo Ruby. Tuy nhiên, cách hiện thực này có hai vấn đề lớn:

  1. Hệ thống của chính bạn có thể bị banh nếu bạn không biết rõ mình đang làm gì: $stdout là một biến toàn cục được sử dụng bởi nhiều thành phần trong hệ thống và bạn thì đang … ghi đè nó.
  2. Khi chạy đồng thời nhiều test case, bạn có thể lỡ tay gom nhầm output của một test case khác. Qua đó giảm tính đúng đắn của cả hai bên.

Là một framework đặt nặng khả năng chạy song song, ExUnit không ghi đè một biến toàn cục nào cả mà hiện thực tính năng này theo một hướng khác dựa trên Erlang IO system.

Erlang IO system

Khi một process cần nhập xuất, nó không tương tác trực tiếp với device (STDOUT, STDERR, socket, …). Thay vào đó nó sẽ gửi request tới IO server. Các IO server này là trung tâm của hệ thống, đảm nhận tương tác với device tương ứng. Mọi thao tác liên quan tới IO server đều được thực hiện thông qua interface :io trong Erlang hoặc IO trong Elixir.

Chi tiết về giao thức IO các bạn có thể xem trên trang chủ của Erlang.

erlang-io-server

Sự phân tách này nhằm khái quát hóa thao tác IO trong lập trình Elixir/Erlang. Mọi process thực hiện nhập xuất bằng cách gửi request tới IO server mà không cần biết bên dưới trao đổi với device thế nào. Khi nhận được request, IO server sẽ đảm nhận tương tác với device tương ứng. Nó có thể xử lý, định dạng sao đó hợp lý, và trả lời lại cho process gửi nếu cần. Mọi thao tác đều xảy ra bất đồng bộ như bản chất của Erlang.

Hơn nữa, sự khái quát hóa này còn giúp dễ dàng tái sử dụng các IO server. Đơn cử như logger và TCP sockets có thể sử dụng chung IO server nếu chúng nhập xuất tới cùng một device.

Bên cạnh đó, việc redirect IO trở nên đơn giản hơn rất nhiều. Ứng dụng điển hình nhất chính là remote console, một tiện ích giúp bạn attach interactive shell vào một Erlang node đang chạy. Một node có thể đang route IO về X nhưng khi bạn thực thi một command trong remote console, IO (prompt/output/log) sẽ được redirect tới chính console đó.

Remote console của Erlang ngoài việc dùng để test code còn làm được một thứ rất “cool” là runtime tracing. Bạn có thể attach một shell vào Erlang node đang chạy và bind tracer vào một process bất kì trong node. Các thông tin tracing sẽ được gửi đến bằng messages. Cơ mà … lan man rồi, tui xin hẹn viết trong một bài khác.

Group leader

Trong Erlang có một khái niệm là group leader. Mặc định mỗi Erlang application khi được start đều thuộc về một group và group có leader. Một group leader có thể thuộc về một group leader khác. Truy cùng vét tận, ta tới được “master process” là group leader tối cao, được bật cùng với Erlang node.

Một Erlang process không nằm chỏng chơ ở một hốc bò tó nào đó trong VM mà luôn thuộc về một group leader. Khi một process con được spawn, nó thừa hưởng group leader từ process cha.

Nhờ group leader, khi tắt một application, máy ảo Erlang biết cách thu hồi bộ nhớ sạch sẽ và tránh được memory leak. Hẳn đọc tới đây có bạn sẽ đặt câu hỏi: “Chẳng phải ông hay rêu rao process trong Erlang được supervise sao?”. Èo, start process bằng spawn/1 thì bạn chỉ có thể supervise nó bằng răng thôi.

Ngoài mục đích thu hồi bộ nhớ, group leader còn là IO server cho STDIO device. Khi môt process gọi IO.puts(:stdio, "message"), một IO request put_chars sẽ được gửi đến group leader của nó. Nếu group leader này thuộc về một group leader khác, nó chỉ đơn giản là forward message lên trên. Và cuối cùng khi đến “master”, message sẽ được gửi đến STDIO device.

CaptureIO

Đến đây chắc bạn cũng có thể tự giải thích cho câu hỏi đầu bài.

Khi test case chạy và yêu cầu capture IO, ExUnit sẽ thay đổi group leader của nó bằng một StringIO server. Mọi IO requests diễn ra sau đó sẽ được redirect về server này. Chúng được gom lại và trả về dưới dạng string khi kết thúc hàm.

defp do_capture_io(:standard_io, options, fun) do
  # lược bỏ vì tui không thích...

  original_gl = Process.group_leader()
  {:ok, capture_gl} = StringIO.open(input, capture_prompt: prompt_config)

  try do
    # đổi group leader.
    Process.group_leader(self(), capture_gl)

    # bắt đầu chạy.
    do_capture_io(capture_gl, fun)
  after
    # cài lại group leader cũ.
    Process.group_leader(self(), original_gl)
  end
end

Thật là tuyệt vời đúng không nào?

Khúc tăng lương

Bài viết này như thường lệ không giúp bạn tăng lương. Cơ mà mong là cùng nhau ta đã biết thêm một chút về vẻ đẹp của Erlang.

Tui sẽ bonus ba hoa thêm một chút về cách Erlang remote shell redirect IO. Khi bạn start một shell, group leader mặc định là chính nó. Như vậy, mọi thao tác với STDIO trong phạm vi shell sở tại đều không ảnh hưởng gì đến hệ thống đang chạy.

Tham khảo

  1. Erlang Rationale của Robert Virding.
  2. The Erlang I/O Protocol.

NGUY HIỂM! KHU VỰC NHIỀU GIÓ!
Khuyến cáo giữ chặt bàn phím và lướt thật nhanh khi đi qua khu vực này.
Chức năng này hỗ trợ markdown và các thứ liên quan.
daipham1210 chém vào lúc

test

Bài viết cùng chủ đề

Nhật ký hốt sh*t—Chuyện về cái service A

Bài viết mà thằng chả chém gió về cách chả monitoring và debug một sự cố gặp phải khi vận hành hệ thống Elixir

IO data và Vectored IO

Bài viết giới thiệu về IO data, Vectored I/O và tối ưu hóa hệ thống dùng Elixir bằng cách tận dụng Vectored I/O.

Poolboy và kĩ thuật pooling trong Erlang/Elixir