
บทความส่วนที่ 6 ว่าด้วยรูปแบบการออกแบบ (patterns) ที่จำเป็นเมื่อนำ OCaml 5 ไปใช้งานจริงใน production systems ครอบคลุม Structured Concurrency, Resource Management, Testing และ Deployment เน้นโค้ดตัวอย่างที่พร้อมใช้งานจริง พร้อมแนวคิดเชิงวิศวกรรมระบบแบบครบวงจร
Structured Concurrency คือหลักการที่ว่า "อายุของ concurrent task ต้องถูกจำกัดอยู่ภายใน lexical scope" เหมือน structured programming ที่ควบคุม control flow ผ่าน block scope (if/while/for) แทน goto
หลักการสำคัญมี 3 ข้อ:
เปรียบเทียบกับ Unstructured Concurrency (เช่น Thread.create แบบเก่า หรือ go func() ใน Go) ซึ่ง goroutine/thread มีอายุเป็นอิสระจากผู้สร้าง ทำให้เกิดปัญหา orphan tasks และ resource leaks ได้ง่าย
%%{init: {'theme':'base','themeVariables':{
'primaryColor':'#3c3836','primaryTextColor':'#ebdbb2','primaryBorderColor':'#fabd2f',
'lineColor':'#fabd2f','secondaryColor':'#504945','tertiaryColor':'#282828',
'background':'#282828','mainBkg':'#3c3836','secondBkg':'#504945',
'textColor':'#ebdbb2','fontFamily':'monospace'
}}}%%
flowchart TB
subgraph Unstructured["แบบดั้งเดิม (Unstructured)"]
U1[Parent
spawn thread] --> U2[Thread A
อายุอิสระ]
U1 --> U3[Thread B
อายุอิสระ]
U1 -.->|return ก่อน
child จบ| U4[Orphan!
Resource Leak]
end
subgraph Structured["Structured Concurrency (Eio Switch)"]
S1[Switch.run] --> S2[Fiber A]
S1 --> S3[Fiber B]
S2 --> S4[Auto Cleanup
ทำความสะอาดอัตโนมัติ]
S3 --> S4
S4 --> S5[Switch จบ
Parent return]
end
style U4 fill:#cc241d,color:#ebdbb2
style S4 fill:#98971a,color:#282828
style S5 fill:#b8bb26,color:#282828
Eio.Switch คือ construct ที่ทำหน้าที่เป็น scope ของ fiber ทั้งหมดที่ spawn อยู่ภายใน เมื่อ Switch.run return fiber ทุกตัวที่ยังทำงานอยู่จะถูก cancel โดยอัตโนมัติ
(* file: examples/switch_basic.ml *)
(* ตัวอย่างพื้นฐานของ Structured Concurrency ด้วย Eio.Switch *)
open Eio
(* งานที่ทำซ้ำ ๆ แล้ว log ผลลัพธ์ *)
let worker ~name ~clock n =
for i = 1 to n do
Time.sleep clock 0.2;
traceln "[%s] iteration %d" name i
done;
traceln "[%s] finished normally" name
let main ~clock =
(* Switch.run สร้าง scope ใหม่ — fiber ทุกตัวที่ fork ใน sw จะผูกกับ scope นี้ *)
Switch.run @@ fun sw ->
traceln ">>> entering switch scope";
(* fork fiber สองตัวแบบ concurrent *)
Fiber.fork ~sw (fun () -> worker ~name:"A" ~clock 5);
Fiber.fork ~sw (fun () -> worker ~name:"B" ~clock 3);
traceln ">>> main fiber continues while children run";
Time.sleep clock 0.5;
traceln ">>> main fiber done, Switch.run จะรอ child ทั้งหมดจบ"
(* ออกจาก scope ที่นี่: Switch.run จะรอ fiber ทุกตัวจบก่อน return *)
(* ตัวอย่างการใช้งาน: รันด้วย dune exec *)
let () =
Eio_main.run @@ fun env ->
main ~clock:(Stdenv.clock env)
(* ผลลัพธ์ที่คาดหวัง (ลำดับอาจสลับกันได้ตาม scheduler):
>>> entering switch scope
>>> main fiber continues while children run
[A] iteration 1
[B] iteration 1
[A] iteration 2
[B] iteration 2
...
[B] finished normally
[A] finished normally
*)
หาก fiber ใดใน Switch raise exception fiber ตัวอื่นจะถูก cancel อัตโนมัติ และ exception จะถูก propagate ออกจาก Switch.run
(* file: examples/switch_failure.ml *)
(* ทดลอง: เมื่อ fiber ตัวหนึ่ง fail ตัวอื่นจะถูก cancel *)
open Eio
exception Worker_failed of string
let long_running ~clock ~name =
(* งานที่ทำนาน 10 วินาที *)
for i = 1 to 10 do
Time.sleep clock 1.0;
traceln "[%s] still alive at %d" name i
done
let buggy_worker ~clock =
Time.sleep clock 1.5;
traceln "[buggy] ❌ ราฟ exception!";
raise (Worker_failed "intentional crash")
let main ~clock =
try
Switch.run @@ fun sw ->
Fiber.fork ~sw (fun () -> long_running ~clock ~name:"A");
Fiber.fork ~sw (fun () -> long_running ~clock ~name:"B");
Fiber.fork ~sw (fun () -> buggy_worker ~clock);
(* Switch จะรอจน buggy_worker raise — A และ B จะถูก cancel *)
with Worker_failed msg ->
traceln "✅ caught at parent: %s" msg
let () =
Eio_main.run @@ fun env ->
main ~clock:(Stdenv.clock env)
(* ผลลัพธ์:
[A] still alive at 1
[B] still alive at 1
[buggy] ❌ ราฟ exception!
✅ caught at parent: intentional crash
(A และ B หยุดที่ iteration 1 เพราะถูก cancel)
*)
เมื่อมี fiber หลายตัว failure อาจเกิดขึ้นพร้อมกันได้ Eio จัดการโดยใช้ Exn.Multiple ซึ่งรวม exception ทั้งหมดเป็น list
(* file: examples/parallel_failures.ml *)
(* การจัดการเมื่อ fiber หลายตัว fail พร้อมกัน *)
open Eio
let flaky_worker ~clock id =
Time.sleep clock (0.1 *. float_of_int id);
if id mod 2 = 0 then
failwith (Printf.sprintf "worker %d crashed" id)
else
traceln "worker %d succeeded" id
let run_all ~clock =
try
Switch.run @@ fun sw ->
for i = 1 to 6 do
Fiber.fork ~sw (fun () -> flaky_worker ~clock i)
done
with
| Exn.Io _ as e ->
traceln "I/O error: %a" Exn.pp e
| Failure msg ->
traceln "single failure: %s" msg
| exn ->
(* Eio.Exn.Multiple wrapper *)
traceln "multiple failures: %s" (Printexc.to_string exn)
let () =
Eio_main.run @@ fun env ->
run_all ~clock:(Stdenv.clock env)
%%{init: {'theme':'base','themeVariables':{
'primaryColor':'#3c3836','primaryTextColor':'#ebdbb2','primaryBorderColor':'#fabd2f',
'lineColor':'#fabd2f','secondaryColor':'#504945','background':'#282828',
'textColor':'#ebdbb2','fontFamily':'monospace'
}}}%%
flowchart LR
A[Fork N Fibers] --> B{Error
Strategy?}
B -->|Fail-Fast| C[Switch.run
default behavior
cancel siblings]
B -->|Collect All| D[Fiber.all
รอทุกตัวจบ
รวม errors]
B -->|Best Effort| E[try/with ใน
แต่ละ fiber]
C --> F[Low Latency
เหมาะกับ critical path]
D --> G[Full Visibility
เหมาะกับ batch jobs]
E --> H[Partial Success
เหมาะกับ fan-out]
style C fill:#fabd2f,color:#282828
style D fill:#b8bb26,color:#282828
style E fill:#83a598,color:#282828
ตารางเปรียบเทียบกลยุทธ์:
| กลยุทธ์ | พฤติกรรม | เหมาะสำหรับ | ตัวอย่าง Use Case |
|---|---|---|---|
| Fail-Fast | ยกเลิกทุก fiber เมื่อตัวแรก fail | งานที่ต้อง atomic | Distributed transaction, consensus |
| Collect-All | รอทุก fiber จบ แล้วรวม error | งาน batch/analytical | Log aggregation, ETL |
| Best Effort | แต่ละ fiber จัดการ error เอง | fan-out อิสระ | Health check, notification |
(* file: examples/best_effort.ml *)
(* Pattern แบบ Best Effort — ไม่มี fiber ใดทำให้ตัวอื่นล้ม *)
open Eio
type result = Ok_ of int | Err of string
let safe_fetch ~clock url_id =
try
Time.sleep clock 0.1;
if url_id = 3 then failwith "network timeout";
Ok_ (url_id * 100)
with Failure msg ->
Err (Printf.sprintf "url %d: %s" url_id msg)
let fan_out ~clock urls =
(* ใช้ Fiber.List.map — รวมผล return จากทุก fiber *)
Fiber.List.map (fun url -> safe_fetch ~clock url) urls
let () =
Eio_main.run @@ fun env ->
let clock = Stdenv.clock env in
let urls = [1; 2; 3; 4; 5] in
let results = fan_out ~clock urls in
List.iter (function
| Ok_ n -> traceln "✅ got %d" n
| Err m -> traceln "⚠️ %s" m
) results
Erlang/OTP เป็นผู้ริเริ่มแนวคิด Supervision Tree ที่ influential ในวงการ systems programming มาก
%%{init: {'theme':'base','themeVariables':{
'primaryColor':'#3c3836','primaryTextColor':'#ebdbb2','primaryBorderColor':'#fabd2f',
'lineColor':'#fabd2f','secondaryColor':'#504945','background':'#282828',
'textColor':'#ebdbb2','fontFamily':'monospace'
}}}%%
flowchart TB
Root[Root Supervisor
ผู้ดูแลสูงสุด]
Root --> DbSup[Database
Supervisor]
Root --> WebSup[Web
Supervisor]
Root --> WorkerSup[Worker Pool
Supervisor]
DbSup --> Conn1[Conn Worker 1]
DbSup --> Conn2[Conn Worker 2]
WebSup --> Http[HTTP Listener]
WebSup --> WS[WebSocket Handler]
WorkerSup --> W1[Job Worker 1]
WorkerSup --> W2[Job Worker 2]
WorkerSup --> W3[Job Worker 3]
style Root fill:#fb4934,color:#282828
style DbSup fill:#fabd2f,color:#282828
style WebSup fill:#fabd2f,color:#282828
style WorkerSup fill:#fabd2f,color:#282828
ตารางเปรียบเทียบ Erlang Supervisor vs Eio Switch:
| คุณสมบัติ (Feature) | Erlang/OTP | Eio (OCaml 5) |
|---|---|---|
| Isolation หน่วยงาน (Unit) | Process (ประมาณ 300 bytes) | Fiber (เบามาก แต่ shared memory) |
| Memory Model | Shared-nothing | Shared memory |
| Failure Isolation | แข็งแรงมาก (process crash อิสระ) | ปานกลาง (shared state อาจเสียหาย) |
| Restart Strategies | one_for_one, one_for_all, rest_for_one |
ต้องเขียนเอง หรือใช้ library |
| Hot Code Reload | รองรับโดย BEAM | ต้อง restart process |
| Type Safety | Dynamic + Dialyzer | Static strong typing |
| Performance | Medium (VM overhead) | High (native code) |
(* file: examples/supervisor.ml *)
(* Supervisor pattern แบบพื้นฐานด้วย Eio + Effects *)
open Eio
(* กลยุทธ์การ restart *)
type restart_strategy =
| One_for_one (* restart เฉพาะ worker ที่ล้ม *)
| One_for_all (* restart ทุก worker ใน supervisor *)
| Rest_for_one (* restart worker ที่ล้มและตัวที่เริ่มหลังจากนั้น *)
type child_spec = {
name : string;
start : clock:float Time.clock_ty r -> sw:Switch.t -> unit -> unit;
max_restarts : int;
}
(* สถิติการ restart *)
type stats = {
mutable restarts : int;
mutable last_error : string option;
}
let make_stats () = { restarts = 0; last_error = None }
(* รัน worker พร้อม restart logic *)
let supervise ~clock ~sw ~strategy ~children =
let all_stats = List.map (fun c -> (c, make_stats ())) children in
let rec run_child (spec, stats) =
if stats.restarts >= spec.max_restarts then begin
traceln "[supervisor] 🛑 %s exhausted max restarts (%d)"
spec.name spec.max_restarts;
()
end else begin
try
traceln "[supervisor] 🚀 starting %s (attempt %d)"
spec.name (stats.restarts + 1);
spec.start ~clock ~sw ()
with exn ->
let msg = Printexc.to_string exn in
stats.restarts <- stats.restarts + 1;
stats.last_error <- Some msg;
traceln "[supervisor] ⚠️ %s crashed: %s" spec.name msg;
(match strategy with
| One_for_one ->
Time.sleep clock 0.5;
run_child (spec, stats)
| One_for_all ->
traceln "[supervisor] 🔄 one_for_all: restarting all";
Time.sleep clock 0.5;
List.iter (fun cs ->
Fiber.fork ~sw (fun () -> run_child cs)
) all_stats
| Rest_for_one ->
traceln "[supervisor] 🔄 rest_for_one: restart this + following";
Time.sleep clock 0.5;
run_child (spec, stats))
end
in
List.iter (fun cs ->
Fiber.fork ~sw (fun () -> run_child cs)
) all_stats
(* ตัวอย่าง child: worker ที่ crash ครั้งแรกแล้วรันปกติ *)
let flaky_worker ~name ~crash_times =
let counter = ref 0 in
fun ~clock ~sw:_ () ->
incr counter;
if !counter <= crash_times then begin
Time.sleep clock 0.3;
failwith (Printf.sprintf "%s crash #%d" name !counter)
end else begin
for i = 1 to 3 do
Time.sleep clock 0.3;
traceln "[%s] working... %d" name i
done
end
let () =
Eio_main.run @@ fun env ->
let clock = Stdenv.clock env in
Switch.run @@ fun sw ->
let children = [
{ name = "db_conn";
start = flaky_worker ~name:"db_conn" ~crash_times:2;
max_restarts = 5 };
{ name = "http_server";
start = flaky_worker ~name:"http_server" ~crash_times:1;
max_restarts = 5 };
{ name = "background_job";
start = flaky_worker ~name:"background_job" ~crash_times:0;
max_restarts = 3 };
] in
supervise ~clock ~sw ~strategy:One_for_one ~children
สมการพื้นฐานของ Exponential Backoff สำหรับ restart delay:
โดย:
(* file: examples/backoff.ml *)
(* Exponential backoff with jitter — ใช้ร่วมกับ supervisor *)
let exponential_backoff ~base ~max_delay ~attempt =
let t = base *. (2.0 ** float_of_int attempt) in
let capped = Float.min t max_delay in
let jitter = Random.float (capped *. 0.3) in (* jitter 0-30% *)
capped +. jitter
(* ตัวอย่าง: backoff ครั้งที่ 0..5 *)
let () =
Random.self_init ();
for n = 0 to 5 do
let d = exponential_backoff ~base:0.1 ~max_delay:10.0 ~attempt:n in
Printf.printf "attempt %d → delay = %.3fs\n" n d
done
(* ผลลัพธ์ตัวอย่าง:
attempt 0 → delay = 0.123s
attempt 1 → delay = 0.234s
attempt 2 → delay = 0.498s
attempt 3 → delay = 0.921s
attempt 4 → delay = 2.012s
attempt 5 → delay = 3.456s *)
ใน concurrent systems resource มีหลายประเภท:
ปัญหาคลาสสิกที่ต้องป้องกัน:
%%{init: {'theme':'base','themeVariables':{
'primaryColor':'#3c3836','primaryTextColor':'#ebdbb2','primaryBorderColor':'#fabd2f',
'lineColor':'#fabd2f','background':'#282828','textColor':'#ebdbb2','fontFamily':'monospace'
}}}%%
flowchart TD
A[Acquire Resource
เปิด file/conn] --> B{Operation
Success?}
B -->|Yes| C[Normal Flow
ใช้ resource]
B -->|No, Exception| D[Cleanup
ทำความสะอาด]
B -->|Cancelled| D
C --> D
D --> E[Release
คืน resource]
E --> F[Continue /
Rethrow]
style A fill:#fabd2f,color:#282828
style D fill:#fb4934,color:#282828
style E fill:#b8bb26,color:#282828
Fun.protect — Try-Finally ของ OCamlOCaml มี Fun.protect ใน stdlib สำหรับ ensure cleanup คล้าย try/finally ในภาษาอื่น
(* signature ของ Fun.protect *)
val protect : finally:(unit -> unit) -> (unit -> 'a) -> 'a
(* รัน finally ทุกกรณี ไม่ว่า f จะจบปกติหรือ raise *)
(* file: examples/fun_protect_basic.ml *)
(* การใช้ Fun.protect พื้นฐาน *)
let read_config path =
let ic = open_in path in
Fun.protect
~finally:(fun () ->
Printf.eprintf "[cleanup] closing %s\n" path;
close_in_noerr ic)
(fun () ->
let line1 = input_line ic in
let line2 = input_line ic in
Printf.sprintf "%s | %s" line1 line2)
(* ตัวอย่างการใช้งาน *)
let () =
(* สร้างไฟล์ทดลอง *)
let oc = open_out "/tmp/config.txt" in
output_string oc "server=localhost\nport=8080\n";
close_out oc;
(* อ่านปลอดภัย *)
try
let s = read_config "/tmp/config.txt" in
print_endline s
with e ->
Printf.eprintf "error: %s\n" (Printexc.to_string e)
หาก finally raise exception ระหว่างที่ main body ก็ raise อยู่แล้ว OCaml จะ raise Fun.Finally_raised wrap exception ของ finally
(* file: examples/fun_protect_double.ml *)
(* กรณีที่ finally เอง raise exception *)
let tricky () =
Fun.protect
~finally:(fun () -> failwith "cleanup failed!")
(fun () -> failwith "main failed!")
let () =
try tricky ()
with
| Failure "main failed!" ->
print_endline "caught main"
| Fun.Finally_raised exn ->
Printf.printf "cleanup exn: %s\n" (Printexc.to_string exn)
| e ->
Printf.printf "other: %s\n" (Printexc.to_string e)
(* Output: cleanup exn: Failure("cleanup failed!")
แสดงว่า exception ของ main body ถูกกลบด้วย finally — ต้องระวัง! *)
Bracket pattern คือการ wrap resource acquisition + release ไว้ในฟังก์ชันเดียวที่รับ callback เป็น "use" — เป็นรูปแบบมาตรฐานในภาษา functional
โดย:
(* file: lib/bracket.ml *)
(* Generic bracket combinator *)
(* [bracket ~acquire ~release use] รัน acquire → use → release เสมอ *)
let bracket ~acquire ~release ~use =
let resource = acquire () in
Fun.protect
~finally:(fun () -> release resource)
(fun () -> use resource)
(* Specialization สำหรับ file handle *)
let with_input_file path ~use =
bracket
~acquire:(fun () -> open_in path)
~release:(fun ic -> close_in_noerr ic)
~use
let with_output_file path ~use =
bracket
~acquire:(fun () -> open_out path)
~release:(fun oc -> close_out_noerr oc)
~use
(* ตัวอย่างใช้: นับบรรทัดในไฟล์ *)
let count_lines path =
with_input_file path ~use:(fun ic ->
let n = ref 0 in
try
while true do
let _ = input_line ic in
incr n
done;
!n
with End_of_file -> !n)
let () =
let oc = open_out "/tmp/sample.txt" in
output_string oc "line1\nline2\nline3\nline4\n";
close_out oc;
Printf.printf "lines: %d\n" (count_lines "/tmp/sample.txt")
(* Output: lines: 4 *)
(* file: lib/bracket_result.ml *)
(* Bracket ที่ return Result แทน raise exception *)
let bracket_r ~acquire ~release ~use =
match acquire () with
| exception exn -> Error (`Acquire exn)
| r ->
let result =
try Ok (use r)
with exn -> Error (`Use exn)
in
(try release r
with exn ->
match result with
| Ok _ -> () (* ignore release error if use succeeded?
หรือควร report — ขึ้นกับ policy *)
| Error _ -> () (* ไม่ override error ของ use *)
|> ignore;
Printf.eprintf "release error: %s\n" (Printexc.to_string exn));
result
(* ตัวอย่างใช้งาน *)
let safe_read path =
bracket_r
~acquire:(fun () -> open_in path)
~release:(fun ic -> close_in ic)
~use:(fun ic -> input_line ic)
let () =
match safe_read "/tmp/sample.txt" with
| Ok line -> Printf.printf "first line: %s\n" line
| Error (`Acquire e) ->
Printf.eprintf "cannot open: %s\n" (Printexc.to_string e)
| Error (`Use e) ->
Printf.eprintf "read failed: %s\n" (Printexc.to_string e)
ใน Eio ทรัพยากรที่ต้อง cleanup ผูกกับ Switch โดยเรียก Switch.on_release — cleanup จะรันเมื่อ Switch จบ (ไม่ว่าจบปกติหรือ error)
(* file: examples/eio_scoped_resource.ml *)
(* Resource ผูกกับ Switch lifetime *)
open Eio
(* Abstract resource: "connection" สมมติ *)
type conn = {
id : int;
mutable closed : bool;
}
let next_id = Atomic.make 0
let open_conn () =
let id = Atomic.fetch_and_add next_id 1 in
traceln "🔌 open conn #%d" id;
{ id; closed = false }
let close_conn c =
if not c.closed then begin
c.closed <- true;
traceln "🔒 close conn #%d" c.id
end
(* bracket style ผ่าน Switch *)
let with_conn ~sw f =
let c = open_conn () in
Switch.on_release sw (fun () -> close_conn c);
f c
let main ~clock =
Switch.run @@ fun sw ->
(* conn หลายตัวใน scope เดียว *)
let c1 = with_conn ~sw Fun.id in
let c2 = with_conn ~sw Fun.id in
traceln "working with conn %d and %d" c1.id c2.id;
Time.sleep clock 0.5
(* ออกจาก Switch.run: c1, c2 จะถูก close อัตโนมัติ
ตามลำดับ LIFO (c2 ก่อน c1) *)
let () =
Eio_main.run @@ fun env ->
main ~clock:(Stdenv.clock env)
(* Output:
🔌 open conn #0
🔌 open conn #1
working with conn 0 and 1
🔒 close conn #1
🔒 close conn #0
*)
%%{init: {'theme':'base','themeVariables':{
'primaryColor':'#3c3836','primaryTextColor':'#ebdbb2','primaryBorderColor':'#fabd2f',
'lineColor':'#fabd2f','background':'#282828','textColor':'#ebdbb2','fontFamily':'monospace'
}}}%%
flowchart TB
subgraph Outer["Outer Switch — อายุยาว"]
DB[DB Pool
long-lived]
subgraph Inner1["Inner Switch 1 — per-request"]
R1[Request Handler 1]
C1[Temp Buffer]
end
subgraph Inner2["Inner Switch 2 — per-request"]
R2[Request Handler 2]
C2[Temp Buffer]
end
end
DB -.->|borrowed| R1
DB -.->|borrowed| R2
style DB fill:#fabd2f,color:#282828
style C1 fill:#83a598,color:#282828
style C2 fill:#83a598,color:#282828
(* file: examples/nested_switches.ml *)
(* Nested switches: outer มีอายุยาว inner สั้น *)
open Eio
type db_pool = { name : string; mutable requests : int }
let open_pool name =
traceln "📦 opening pool %s" name;
{ name; requests = 0 }
let close_pool p =
traceln "📦 closing pool %s (handled %d reqs)" p.name p.requests
let with_pool ~sw name f =
let p = open_pool name in
Switch.on_release sw (fun () -> close_pool p);
f p
let handle_request ~clock ~pool req_id =
Switch.run @@ fun req_sw ->
(* per-request resources *)
let buf = Bytes.create 1024 in
Switch.on_release req_sw (fun () ->
traceln "🧹 cleaning buffer for req %d" req_id);
pool.requests <- pool.requests + 1;
traceln "⚙️ req %d using pool %s (buf size=%d)"
req_id pool.name (Bytes.length buf);
Time.sleep clock 0.1
let main ~clock =
(* outer switch: pool lives here *)
Switch.run @@ fun outer_sw ->
let pool = with_pool ~sw:outer_sw "postgres" Fun.id in
(* handle 3 requests *)
Fiber.all (List.init 3 (fun i ->
fun () -> handle_request ~clock ~pool i))
let () =
Eio_main.run @@ fun env ->
main ~clock:(Stdenv.clock env)
(* file: lib/conn_pool.ml *)
(* Bounded connection pool ที่ thread-safe สำหรับ Eio *)
open Eio
module Make (R : sig
type t
val create : unit -> t
val close : t -> unit
val is_valid : t -> bool
end) = struct
type t = {
max_size : int;
mutable current : int;
available : R.t Queue.t;
mutex : Mutex.t;
cond : Condition.t;
}
let create ~max_size = {
max_size;
current = 0;
available = Queue.create ();
mutex = Mutex.create ();
cond = Condition.create ();
}
(* ขอ connection แบบ blocking จนกว่าจะมีว่าง *)
let acquire pool =
Mutex.lock pool.mutex;
Fun.protect
~finally:(fun () -> Mutex.unlock pool.mutex)
(fun () ->
(* รอจน (มีของใน queue) หรือ (ยังสร้างใหม่ได้) *)
while Queue.is_empty pool.available
&& pool.current >= pool.max_size do
Condition.wait pool.cond pool.mutex
done;
match Queue.take_opt pool.available with
| Some r when R.is_valid r -> r
| Some r ->
R.close r;
pool.current <- pool.current - 1;
let fresh = R.create () in
pool.current <- pool.current + 1;
fresh
| None ->
let fresh = R.create () in
pool.current <- pool.current + 1;
fresh)
let release pool r =
Mutex.lock pool.mutex;
if R.is_valid r then
Queue.add r pool.available
else begin
R.close r;
pool.current <- pool.current - 1
end;
Condition.signal pool.cond;
Mutex.unlock pool.mutex
(* safe wrapper: ensure release แม้ use จะ raise *)
let with_conn pool ~use =
let r = acquire pool in
Fun.protect
~finally:(fun () -> release pool r)
(fun () -> use r)
let shutdown pool =
Mutex.lock pool.mutex;
Queue.iter R.close pool.available;
Queue.clear pool.available;
pool.current <- 0;
Mutex.unlock pool.mutex
end
(* ตัวอย่างการ instantiate *)
module Fake_db = struct
type t = { id : int; mutable alive : bool }
let counter = Atomic.make 0
let create () =
let id = Atomic.fetch_and_add counter 1 in
Printf.printf "[db] opened #%d\n" id;
{ id; alive = true }
let close t =
Printf.printf "[db] closed #%d\n" t.id;
t.alive <- false
let is_valid t = t.alive
end
module Pool = Make(Fake_db)
let () =
let pool = Pool.create ~max_size:3 in
(* simulation การใช้งาน *)
for i = 1 to 5 do
Pool.with_conn pool ~use:(fun conn ->
Printf.printf "job %d using conn #%d\n" i conn.Fake_db.id)
done;
Pool.shutdown pool
ปัญหา: หาก fiber ถูก cancel ระหว่าง acquire แต่ก่อน Switch.on_release → resource leak!
แก้: ใช้ Switch.on_release ก่อน operation ที่อาจ yield หรือ ใช้ helper ที่ atomic
(* file: examples/cancel_safe.ml *)
(* Pattern ที่ cancel-safe — register cleanup ก่อน yield *)
open Eio
type socket = { fd : int; mutable open_ : bool }
let fake_socket_open () =
traceln "🌐 socket open";
{ fd = 42; open_ = true }
let fake_socket_close s =
if s.open_ then begin
s.open_ <- false;
traceln "🌐 socket close"
end
(* ❌ UNSAFE: หาก cancel เกิดระหว่าง sleep → leak *)
let unsafe_connect ~sw ~clock =
let s = fake_socket_open () in
Time.sleep clock 0.5; (* <-- cancellation point! *)
Switch.on_release sw (fun () -> fake_socket_close s);
s
(* ✅ SAFE: register cleanup ทันที *)
let safe_connect ~sw ~clock =
let s = fake_socket_open () in
Switch.on_release sw (fun () -> fake_socket_close s);
(* แม้ cancel เกิดตรงนี้ socket จะถูก close *)
Time.sleep clock 0.5;
s
let () =
Eio_main.run @@ fun env ->
let clock = Stdenv.clock env in
try
Switch.run @@ fun sw ->
Fiber.fork ~sw (fun () ->
let _s = safe_connect ~sw ~clock in
traceln "connected!");
(* ยกเลิกหลัง 0.1 วินาที — ก่อน sleep จบ *)
Time.sleep clock 0.1;
failwith "simulated abort"
with Failure _ ->
traceln "switch closed — socket ได้รับการ close แล้ว"
| Check | รายละเอียด |
|---|---|
| ✅ Register cleanup ทันที | ใช้ Switch.on_release ทันทีหลัง acquire ก่อนมี yield |
| ✅ ใช้ bracket/protect | อย่าเขียน close เอง — ใช้ combinator ที่ ensure cleanup |
| ✅ Bounded Pools | limit จำนวน resource เสมอเพื่อกัน DoS |
| ✅ Timeout ทุก operation | ใช้ Time.with_timeout หรือ Fiber.first |
| ✅ Monitor resource counts | export metrics (prometheus) เพื่อจับ leak |
| ✅ Test cancellation paths | เขียน test ยิง cancel ระหว่างทุก step |
%%{init: {'theme':'base','themeVariables':{
'primaryColor':'#3c3836','primaryTextColor':'#ebdbb2','primaryBorderColor':'#fabd2f',
'lineColor':'#fabd2f','background':'#282828','textColor':'#ebdbb2','fontFamily':'monospace'
}}}%%
flowchart TB
subgraph Bottom["ระดับต่ำ — Fast & Narrow"]
UT[Unit Tests
Alcotest]
end
subgraph Middle["ระดับกลาง — Property Coverage"]
PBT[Property-Based
QCheck]
FUZZ[Fuzzing
crowbar/afl]
end
subgraph High["ระดับสูง — Concurrency & Integration"]
LIN[Linearizability
Lin/Ortac]
CONC[Concurrency
Simulation]
INT[Integration
real deps]
end
UT --> PBT
UT --> FUZZ
PBT --> LIN
FUZZ --> LIN
LIN --> CONC
CONC --> INT
style UT fill:#b8bb26,color:#282828
style PBT fill:#fabd2f,color:#282828
style FUZZ fill:#fabd2f,color:#282828
style LIN fill:#fb4934,color:#282828
style CONC fill:#fb4934,color:#282828
style INT fill:#d3869b,color:#282828
Alcotest เป็น lightweight test framework ที่ simple และ pretty output — เหมาะสำหรับงาน unit test
my_project/
├── dune-project
├── lib/
│ ├── dune
│ └── my_lib.ml
└── test/
├── dune
└── test_my_lib.ml
(* file: lib/my_lib.ml *)
(* โมดูลตัวอย่างที่จะทดสอบ *)
let add a b = a + b
let divide a b =
if b = 0 then Error "division by zero"
else Ok (a / b)
(* Stack แบบ mutable *)
type 'a stack = { mutable items : 'a list }
let make_stack () = { items = [] }
let push s x = s.items <- x :: s.items
let pop s =
match s.items with
| [] -> None
| x :: rest -> s.items <- rest; Some x
let size s = List.length s.items
; file: lib/dune
(library
(name my_lib))
(* file: test/test_my_lib.ml *)
(* Unit tests ด้วย Alcotest *)
open Alcotest
(* Test สำหรับ add *)
let test_add_positive () =
check int "2+3" 5 (My_lib.add 2 3)
let test_add_zero () =
check int "0+0" 0 (My_lib.add 0 0)
let test_add_negative () =
check int "(-5) + 10" 5 (My_lib.add (-5) 10)
(* Test สำหรับ divide — ใช้ result testable *)
let result_int_string =
result int string
let test_divide_ok () =
check result_int_string "10/2" (Ok 5) (My_lib.divide 10 2)
let test_divide_by_zero () =
check result_int_string "10/0"
(Error "division by zero") (My_lib.divide 10 0)
(* Test สำหรับ stack — stateful *)
let test_stack_push_pop () =
let s = My_lib.make_stack () in
My_lib.push s 1;
My_lib.push s 2;
My_lib.push s 3;
check int "size after 3 pushes" 3 (My_lib.size s);
check (option int) "pop 3" (Some 3) (My_lib.pop s);
check (option int) "pop 2" (Some 2) (My_lib.pop s);
check (option int) "pop 1" (Some 1) (My_lib.pop s);
check (option int) "pop empty" None (My_lib.pop s)
(* รวม tests เป็น suite *)
let () =
run "My_lib" [
"add", [
test_case "positive" `Quick test_add_positive;
test_case "zero" `Quick test_add_zero;
test_case "negative" `Quick test_add_negative;
];
"divide", [
test_case "ok" `Quick test_divide_ok;
test_case "by zero" `Quick test_divide_by_zero;
];
"stack", [
test_case "push/pop" `Quick test_stack_push_pop;
];
]
; file: test/dune
(test
(name test_my_lib)
(libraries my_lib alcotest))
การรัน:
$ dune runtest
Testing `My_lib'.
This run has ID `ABC123'.
[OK] add 0 positive.
[OK] add 1 zero.
[OK] add 2 negative.
[OK] divide 0 ok.
[OK] divide 1 by zero.
[OK] stack 0 push/pop.
Full test results in `.../_build/_tests/My_lib'.
Test Successful in 0.003s. 6 tests run.
Property-based testing คือการ generate input แบบ random จำนวนมาก (ปกติ 100–10000 cases) แล้วตรวจว่า property ยังเป็นจริง
(* file: test/test_properties.ml *)
(* Property-based tests ด้วย QCheck *)
open QCheck2
(* Property 1: add เป็น commutative *)
let prop_add_commutative =
Test.make
~name:"add is commutative"
~count:1000
Gen.(pair int int)
(fun (a, b) -> My_lib.add a b = My_lib.add b a)
(* Property 2: add เป็น associative *)
let prop_add_associative =
Test.make
~name:"add is associative"
~count:1000
Gen.(triple int int int)
(fun (a, b, c) ->
My_lib.add (My_lib.add a b) c
= My_lib.add a (My_lib.add b c))
(* Property 3: push แล้ว pop ได้ค่าเดิม (stack invariant) *)
let prop_push_pop =
Test.make
~name:"push then pop returns same value"
~count:500
Gen.int
(fun x ->
let s = My_lib.make_stack () in
My_lib.push s x;
My_lib.pop s = Some x)
(* Property 4: reverse twice = identity *)
let prop_reverse_involutive =
Test.make
~name:"reverse is involutive"
~count:500
Gen.(list int)
(fun xs -> List.rev (List.rev xs) = xs)
(* Property 5: sort แล้ว length เท่าเดิม *)
let prop_sort_preserves_length =
Test.make
~name:"sort preserves length"
~count:500
Gen.(list int)
(fun xs -> List.length (List.sort compare xs) = List.length xs)
(* Property 6: sorted list เป็น non-decreasing *)
let is_sorted = function
| [] | [_] -> true
| xs ->
let rec aux = function
| a :: (b :: _ as rest) -> a <= b && aux rest
| _ -> true
in aux xs
let prop_sort_sorted =
Test.make
~name:"sort produces sorted list"
~count:500
Gen.(list small_int)
(fun xs -> is_sorted (List.sort compare xs))
let () =
let tests = [
prop_add_commutative;
prop_add_associative;
prop_push_pop;
prop_reverse_involutive;
prop_sort_preserves_length;
prop_sort_sorted;
] in
let alcotests = List.map QCheck_alcotest.to_alcotest tests in
Alcotest.run "properties" [
"qcheck", alcotests
]
เมื่อ QCheck พบ counterexample มันจะพยายาม shrink ให้เล็กที่สุด:
(* file: test/test_shrinking.ml *)
(* ตัวอย่าง property ที่จะ fail เพื่อดู shrinking *)
open QCheck2
(* ฟังก์ชันที่ buggy: "sum จะไม่เกิน n*100 เมื่อ n <= list length" *)
let buggy_sum_bound =
Test.make
~name:"sum bounded (buggy property)"
~count:100
Gen.(list small_int)
(fun xs ->
let sum = List.fold_left (+) 0 xs in
(* property นี้ผิดเมื่อ list มีค่าใหญ่ *)
sum < List.length xs * 50)
(* รันเพื่อดูว่า QCheck จะ shrink อย่างไร *)
let () =
QCheck_base_runner.run_tests_main [buggy_sum_bound]
(* QCheck จะรายงาน:
test 'sum bounded (buggy property)' failed on input: [87; 99; 45]
(shrank from longer list) *)
(* file: test/test_custom_gen.ml *)
(* สร้าง generator เอง สำหรับ domain model *)
open QCheck2
(* Domain: user account *)
type user = {
id : int;
name : string;
age : int;
email : string;
}
let gen_email =
let open Gen in
let* local = string_size ~gen:(char_range 'a' 'z') (1 -- 10) in
let* domain = oneofl ["example.com"; "test.org"; "mail.io"] in
return (local ^ "@" ^ domain)
let gen_user =
let open Gen in
let* id = 1 -- 1_000_000 in
let* name = string_size ~gen:(char_range 'A' 'Z') (3 -- 15) in
let* age = 0 -- 120 in
let* email = gen_email in
return { id; name; age; email }
(* Property: user ที่ generate ทุกคนต้องมี email ที่ถูกต้อง *)
let prop_valid_email =
Test.make
~name:"generated users have @ in email"
~count:200
gen_user
(fun u -> String.contains u.email '@')
let () =
QCheck_base_runner.run_tests_main [prop_valid_email]
Linearizability Testing คือการตรวจสอบว่า concurrent data structure ทำงานเหมือน sequential version ราวกับ operation เกิดขึ้นทีละตัว (แม้จริง ๆ จะ interleave)
Lin คือ library ของ OCaml 5 (จาก Tarides) ที่ generate test interleaving จำนวนมาก แล้วตรวจว่าผลลัพธ์ linearizable
(* file: test/test_counter_lin.ml *)
(* Linearizability test สำหรับ concurrent counter *)
(* Module ที่จะทดสอบ: atomic counter *)
module Counter = struct
type t = int Atomic.t
let make () = Atomic.make 0
let incr t = Atomic.incr t
let get t = Atomic.get t
let reset t = Atomic.set t 0
end
(* Describe API ให้ Lin *)
module Spec = struct
type t = Counter.t
let init () = Counter.make ()
let cleanup _ = ()
open Lin
let api = [
val_ "Counter.incr" Counter.incr (t @-> returning unit);
val_ "Counter.get" Counter.get (t @-> returning int);
val_ "Counter.reset" Counter.reset (t @-> returning unit);
]
end
module Lin_test = Lin_domain.Make(Spec)
let () =
QCheck_base_runner.run_tests_main [
Lin_test.neg_lin_test ~count:1000 ~name:"Counter is linearizable"
]
(* Lin จะ:
1. generate sequence ของ operations random
2. รัน operations ใน 2+ domains พร้อมกัน
3. พยายามหา linearization ที่ match กับผลลัพธ์จริง
4. fail ถ้าไม่มี linearization ที่ valid *)
(* file: test/test_buggy_counter.ml *)
(* Counter ที่ buggy — ไม่ใช้ Atomic *)
module Buggy_counter = struct
type t = { mutable v : int }
let make () = { v = 0 }
let incr t = t.v <- t.v + 1 (* ❌ race condition! *)
let get t = t.v
end
module Spec = struct
type t = Buggy_counter.t
let init () = Buggy_counter.make ()
let cleanup _ = ()
open Lin
let api = [
val_ "incr" Buggy_counter.incr (t @-> returning unit);
val_ "get" Buggy_counter.get (t @-> returning int);
]
end
module Test = Lin_domain.Make(Spec)
let () =
(* Lin จะพบว่าหลัง incr 100 ครั้งจาก 2 domains
get อาจได้ค่าน้อยกว่า 200 → ไม่ linearizable *)
QCheck_base_runner.run_tests_main [
Test.neg_lin_test ~count:5000 ~name:"Buggy counter"
]
Fuzzing = ป้อน input ที่ unexpected/malformed เข้า system เพื่อหา crash หรือ vulnerability
Crowbar ใช้ afl-fuzz backend + QCheck-style property
(* file: test/fuzz_parser.ml *)
(* Fuzz test: JSON parser ไม่ควร crash ไม่ว่า input จะเป็นอะไร *)
open Crowbar
(* Generator ของ bytes แบบไม่จำกัด structure *)
let bytes_gen = bytes
(* Property: parse ต้องจบ (return Ok หรือ Error) ไม่ควร crash *)
let () =
add_test ~name:"json parser never crashes" [bytes_gen]
(fun input ->
match Yojson.Safe.from_string input with
| _ -> () (* ok *)
| exception Yojson.Json_error _ -> () (* ok — expected parse error *)
| exception e ->
(* ❌ unexpected exception — bug! *)
failf "unexpected: %s" (Printexc.to_string e))
การรัน fuzzing:
# 1. build with AFL instrumentation
$ dune build --instrument-with afl
# 2. สร้าง corpus เริ่มต้น
$ mkdir -p corpus && echo '{"hello":"world"}' > corpus/seed1
# 3. รัน afl-fuzz
$ afl-fuzz -i corpus -o findings -- ./_build/default/test/fuzz_parser.exe @@
การทดสอบ code ที่ใช้ Eio มี challenge เพราะต้องการ event loop — ใช้ Eio_mock สำหรับ time manipulation
(* file: test/test_eio_code.ml *)
(* ทดสอบ code ที่ใช้ Eio clock ด้วย mock clock *)
open Eio
(* โค้ดที่ต้องทดสอบ: timeout wrapper *)
let fetch_with_timeout ~clock ~timeout f =
Time.with_timeout clock timeout f
(* Helper สำหรับ test: simulate ทำงาน n วินาที *)
let slow_task ~clock duration () =
Time.sleep clock duration;
Ok "done"
let test_within_timeout () =
Eio_mock.Backend.run @@ fun () ->
let clock = Eio_mock.Clock.make () in
let result =
Fiber.both
(fun () ->
(* task ใช้ 1 วินาที timeout 5 วินาที → ต้องสำเร็จ *)
let r = fetch_with_timeout ~clock ~timeout:5.0
(slow_task ~clock:(clock :> _ Time.clock_ty r) 1.0) in
Alcotest.(check (result string string))
"within timeout" (Ok "done") r)
(fun () ->
Eio_mock.Clock.advance clock)
in
result
let test_exceeds_timeout () =
Eio_mock.Backend.run @@ fun () ->
let clock = Eio_mock.Clock.make () in
Fiber.both
(fun () ->
let r = fetch_with_timeout ~clock ~timeout:1.0
(slow_task ~clock:(clock :> _ Time.clock_ty r) 5.0) in
Alcotest.(check bool)
"exceeds timeout"
true
(Result.is_error r))
(fun () ->
Eio_mock.Clock.advance clock)
let () =
Alcotest.run "Eio" [
"timeout", [
Alcotest.test_case "within" `Quick test_within_timeout;
Alcotest.test_case "exceeds" `Quick test_exceeds_timeout;
]
]
| ประเภท Test | Tool | เมื่อไหร่ใช้ | ความเร็ว |
|---|---|---|---|
| Unit test | Alcotest | Pure function, data transformation | เร็วมาก (ms) |
| Property-based | QCheck | Invariants, algebraic laws | เร็ว (s) |
| Concurrency | Lin / Ortac | Lock-free structures, atomic ops | ปานกลาง (min) |
| Fuzzing | Crowbar + AFL | Parsers, input validation, security | ช้า (hours+) |
| Integration | Alcotest + real deps | End-to-end flow, DB, network | ช้า (s-min) |
| Mock time | Eio_mock | Code ที่ใช้ clock/timeout | เร็ว (ms) |
%%{init: {'theme':'base','themeVariables':{
'primaryColor':'#3c3836','primaryTextColor':'#ebdbb2','primaryBorderColor':'#fabd2f',
'lineColor':'#fabd2f','background':'#282828','textColor':'#ebdbb2','fontFamily':'monospace'
}}}%%
flowchart LR
SRC[Source Code
ml/mli files] --> LOCK[opam.locked
reproducible deps]
LOCK --> BUILD[dune build
--release]
BUILD --> BIN[Native Binary
~5-50MB]
BIN --> STATIC{Static
Link?}
STATIC -->|Yes| MUSL[musl-libc
fully static]
STATIC -->|No| DYN[glibc dynamic]
MUSL --> DOCKER[Docker Image
FROM scratch
~10MB]
DYN --> DOCKER_ALPINE[Docker Image
FROM alpine
~50MB]
DOCKER --> K8S[Kubernetes /
systemd]
DOCKER_ALPINE --> K8S
style BIN fill:#fabd2f,color:#282828
style MUSL fill:#b8bb26,color:#282828
style DOCKER fill:#83a598,color:#282828
ทำไม static linking?
scratch (เล็กมาก)# 1. สร้าง opam switch สำหรับ musl static build
$ opam switch create 5.2.0+musl+static \
--packages=ocaml-variants.5.2.0+options,ocaml-option-musl,ocaml-option-static
# 2. install deps
$ opam install . --deps-only --locked
# 3. build with static flag
$ dune build --release --profile=static
; file: dune-project
(lang dune 3.14)
(name myapp)
(generate_opam_files true)
(package
(name myapp)
(depends
(ocaml (>= 5.2.0))
(dune (>= 3.14))
(eio_main (>= 1.0))
(cmdliner (>= 1.2))
(logs (>= 0.7))))
; file: bin/dune
(executable
(name main)
(public_name myapp)
(libraries myapp_lib eio_main cmdliner logs logs.fmt)
(flags (:standard))
; เพิ่ม link flags เมื่อ build ด้วย profile=static
(ocamlopt_flags
(:standard
(when %{profile} = static
(-cclib -static -cclib -no-pie)))))
(* file: bin/main.ml *)
(* Entrypoint สำหรับ production app *)
open Cmdliner
let setup_logs level =
Logs.set_reporter (Logs_fmt.reporter ());
Logs.set_level (Some level);
()
let run port verbose =
let level = if verbose then Logs.Debug else Logs.Info in
setup_logs level;
Logs.info (fun m -> m "🚀 starting on port %d" port);
Eio_main.run @@ fun env ->
Myapp_lib.Server.run ~env ~port
let port =
let doc = "Port to listen on" in
Arg.(value & opt int 8080 & info ["p"; "port"] ~doc)
let verbose =
let doc = "Enable debug logging" in
Arg.(value & flag & info ["v"; "verbose"] ~doc)
let cmd =
let doc = "Sample OCaml 5 production service" in
let info = Cmd.info "myapp" ~version:"0.1.0" ~doc in
Cmd.v info Term.(const run $ port $ verbose)
let () = exit (Cmd.eval cmd)
Multi-stage build แยก stage build ออกจาก stage runtime ลดขนาด image อย่างมาก
# file: Dockerfile
# ============================================
# Stage 1: Builder — มีเครื่องมือ build ครบ
# ============================================
FROM ocaml/opam:alpine-3.19-ocaml-5.2 AS builder
# ติดตั้ง dev deps ที่ opam packages ต้องการ
USER root
RUN apk add --no-cache \
gmp-dev \
libev-dev \
openssl-dev \
pkgconf \
linux-headers \
musl-dev \
gcc \
g++ \
make
USER opam
WORKDIR /home/opam/app
# Copy opam files ก่อน → cache layer นี้จน deps เปลี่ยน
COPY --chown=opam:opam myapp.opam dune-project ./
COPY --chown=opam:opam myapp.opam.locked ./
# ติดตั้ง deps ตาม lockfile (reproducible)
RUN opam install . --deps-only --locked --yes
# Copy source แล้ว build release
COPY --chown=opam:opam . .
RUN eval $(opam env) && \
dune build --release bin/main.exe && \
cp _build/default/bin/main.exe /home/opam/myapp && \
strip /home/opam/myapp
# ============================================
# Stage 2: Runtime — เล็กที่สุด
# ============================================
FROM alpine:3.19 AS runtime
RUN apk add --no-cache \
gmp \
libev \
openssl \
ca-certificates \
&& addgroup -S app \
&& adduser -S -G app -u 10001 app
COPY --from=builder /home/opam/myapp /usr/local/bin/myapp
USER app
EXPOSE 8080
# Health check ผ่าน endpoint /health
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD wget -q -O /dev/null http://localhost:8080/health || exit 1
ENTRYPOINT ["/usr/local/bin/myapp"]
CMD ["--port", "8080"]
# file: Dockerfile.static
# Image สุดท้าย ~ 10-15 MB ไม่มี OS layer เลย
FROM ocaml/opam:alpine-3.19-ocaml-5.2 AS builder
USER root
RUN apk add --no-cache gmp-dev libev-dev openssl-dev pkgconf \
linux-headers musl-dev gcc g++ make
WORKDIR /app
COPY --chown=opam:opam . .
USER opam
# ใช้ musl+static switch
RUN opam switch create . 5.2.0+musl+static \
--packages=ocaml-variants.5.2.0+options,ocaml-option-musl,ocaml-option-static \
--yes && \
opam install . --deps-only --locked --yes && \
eval $(opam env) && \
dune build --release --profile=static bin/main.exe && \
strip _build/default/bin/main.exe
# -------- runtime from scratch --------
FROM scratch
COPY --from=builder /app/_build/default/bin/main.exe /myapp
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
EXPOSE 8080
ENTRYPOINT ["/myapp"]
| Base Image | ขนาดโดยประมาณ | เหมาะกับ |
|---|---|---|
FROM scratch + static |
~10-15 MB | Production, min attack surface |
FROM alpine:3.19 |
~25-50 MB | ต้อง shell debug, common libs |
FROM debian:slim |
~80-120 MB | ต้อง glibc, compatibility |
FROM ubuntu:22.04 |
~200+ MB | Dev/debug ไม่เหมาะ prod |
การ cross-compile OCaml ทำได้หลายวิธี:
# build สำหรับ aarch64 บน x86_64 host ด้วย buildx
$ docker buildx create --use --name multi-arch
$ docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag myrepo/myapp:0.1.0 \
--push \
.
# Makefile helper
# file: Makefile
IMAGE := myrepo/myapp
VERSION := $(shell cat VERSION)
.PHONY: build-multiarch
build-multiarch:
docker buildx build \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
--tag $(IMAGE):$(VERSION) \
--tag $(IMAGE):latest \
--push \
.
.PHONY: build-local
build-local:
docker build -t $(IMAGE):dev .
.PHONY: run
run: build-local
docker run --rm -p 8080:8080 $(IMAGE):dev
Lock file บันทึก version ที่ถูกทดสอบแล้วของ ทุก transitive dependency เพื่อให้ build ได้ผลเหมือนเดิมทุกครั้ง
# สร้าง lockfile จาก current environment
$ opam lock .
# จะได้ไฟล์ myapp.opam.locked ที่ pin ทุก version:
# opam-version: "2.0"
# depends: [
# "ocaml" {= "5.2.0"}
# "dune" {= "3.14.0"}
# "eio" {= "1.0"}
# "eio_main" {= "1.0"}
# ...
# ]
# ติดตั้งจาก lockfile (reproducible)
$ opam install . --locked --deps-only
# update specific package แล้วสร้าง lock ใหม่
$ opam update
$ opam upgrade eio
$ opam lock .
$ git commit myapp.opam.locked -m "bump eio"
# file: .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
ocaml: ['5.2.x']
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Setup OCaml ${{ matrix.ocaml }}
uses: ocaml/setup-ocaml@v3
with:
ocaml-compiler: ${{ matrix.ocaml }}
opam-pin: false
- name: Install deps (locked)
run: opam install . --deps-only --with-test --locked
- name: Build
run: opam exec -- dune build --release
- name: Run tests
run: opam exec -- dune runtest
- name: Run property tests
run: opam exec -- dune exec test/test_properties.exe
docker:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }}
- uses: docker/build-push-action@v5
with:
platforms: linux/amd64,linux/arm64
push: true
tags: myrepo/myapp:${{ github.sha }},myrepo/myapp:latest
สำหรับการ deploy แบบ bare metal หรือ VM systemd เป็น de-facto init system บน Linux
; file: /etc/systemd/system/myapp.service
[Unit]
Description=MyApp — OCaml 5 service
After=network-online.target
Wants=network-online.target
Documentation=https://github.com/myorg/myapp
[Service]
Type=exec
User=myapp
Group=myapp
ExecStart=/usr/local/bin/myapp --port 8080
Restart=on-failure
RestartSec=5s
TimeoutStopSec=30s
; ======= Hardening =======
; จำกัดสิทธิ์เพื่อความปลอดภัย
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=true
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictRealtime=true
SystemCallArchitectures=native
; Working directory ที่เขียนได้
StateDirectory=myapp
WorkingDirectory=/var/lib/myapp
; ======= Resource Limits =======
LimitNOFILE=65536
LimitNPROC=4096
MemoryMax=2G
CPUQuota=200%
; ======= Environment =======
Environment="OCAMLRUNPARAM=b,s=32M"
EnvironmentFile=-/etc/myapp/environment
[Install]
WantedBy=multi-user.target
การ deploy:
# 1. copy binary และ unit file
$ sudo cp ./myapp /usr/local/bin/
$ sudo cp ./myapp.service /etc/systemd/system/
# 2. create user (if not exists)
$ sudo useradd -r -s /sbin/nologin myapp
# 3. reload systemd และ start
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now myapp
$ sudo systemctl status myapp
# 4. follow logs
$ journalctl -u myapp -f
; Environment variables ที่สำคัญสำหรับ tuning
OCAMLRUNPARAM="b,s=32M,h=1G,o=120"
; b → print backtrace on uncaught exception
; s=32M → minor heap size (default 256K) — ใหญ่ขึ้นลด GC frequency
; h=1G → initial major heap size
; o=120 → space_overhead % — สูงขึ้น GC น้อยลง แต่ memory เยอะขึ้น
; v=15 → verbose GC debug (development only)
โดย:
เช่น ถ้า live data = 100MB, space_overhead = 120% → heap จะขยายเป็น 220MB ก่อน trigger major GC รอบใหม่
Production service ที่สมบูรณ์ต้องมี 3 pillars ของ observability:
%%{init: {'theme':'base','themeVariables':{
'primaryColor':'#3c3836','primaryTextColor':'#ebdbb2','primaryBorderColor':'#fabd2f',
'lineColor':'#fabd2f','background':'#282828','textColor':'#ebdbb2','fontFamily':'monospace'
}}}%%
flowchart LR
APP[OCaml App]
APP --> LOGS[Structured Logs
logs + fmt
JSON output]
APP --> METRICS[Metrics
prometheus
/metrics endpoint]
APP --> TRACES[Traces
opentelemetry-ocaml]
LOGS --> LOKI[Loki]
METRICS --> PROM[Prometheus]
TRACES --> JAEGER[Jaeger/Tempo]
LOKI --> GRAFANA[Grafana]
PROM --> GRAFANA
JAEGER --> GRAFANA
style APP fill:#fabd2f,color:#282828
style GRAFANA fill:#b8bb26,color:#282828
(* file: lib/obs.ml *)
(* Observability helpers *)
(* Structured JSON reporter สำหรับ logs *)
let json_reporter () =
let report src level ~over k msgf =
let k _ = over (); k () in
msgf @@ fun ?header:_ ?tags:_ fmt ->
let time = Unix.gettimeofday () in
let level_str = match level with
| Logs.App -> "app"
| Logs.Error -> "error"
| Logs.Warning -> "warn"
| Logs.Info -> "info"
| Logs.Debug -> "debug"
in
let src_name = Logs.Src.name src in
Format.kasprintf (fun msg ->
Printf.printf
{|{"time":%.3f,"level":"%s","source":"%s","msg":%s}
|}
time level_str src_name (Yojson.Safe.to_string (`String msg));
k ()
) fmt
in
{ Logs.report }
let setup ~json ~level =
if json then
Logs.set_reporter (json_reporter ())
else
Logs.set_reporter (Logs_fmt.reporter ());
Logs.set_level (Some level)
(* ตัวอย่างใช้งาน *)
let src = Logs.Src.create "myapp.http" ~doc:"HTTP server"
module Log = (val Logs.src_log src : Logs.LOG)
let handle_request req =
Log.info (fun m -> m "request from %s path=%s" req.ip req.path);
try
let resp = process req in
Log.debug (fun m -> m "response size=%d" resp.size);
resp
with e ->
Log.err (fun m -> m "request failed: %s" (Printexc.to_string e));
raise e
| หมวด | รายการตรวจ | สถานะ |
|---|---|---|
| Build | ใช้ opam.locked | ⬜ |
Build ด้วย dune --release |
⬜ | |
| Strip symbols จาก binary | ⬜ | |
| มี Dockerfile multi-stage | ⬜ | |
| Image ขนาด < 50MB | ⬜ | |
| Runtime | ตั้ง OCAMLRUNPARAM tuning | ⬜ |
| Non-root user | ⬜ | |
| ulimit เหมาะสม (nofile >= 65536) | ⬜ | |
| Graceful shutdown on SIGTERM | ⬜ | |
| Observability | Structured logs (JSON) | ⬜ |
| Metrics endpoint (/metrics) | ⬜ | |
| Health check endpoint (/health) | ⬜ | |
| Readiness check (/ready) | ⬜ | |
| Tracing (OpenTelemetry) | ⬜ | |
| Security | Dependencies audit (opam audit) | ⬜ |
| Read-only filesystem | ⬜ | |
| Secrets ผ่าน env/mount ไม่ hardcode | ⬜ | |
| TLS termination | ⬜ | |
| Reliability | Health check configured | ⬜ |
| Resource limits (CPU/memory) | ⬜ | |
| Restart policy | ⬜ | |
| Backup strategy | ⬜ | |
| Testing | Unit test coverage > 80% | ⬜ |
| Property tests สำหรับ critical logic | ⬜ | |
| Linearizability test สำหรับ concurrent | ⬜ | |
| Load test ก่อน release | ⬜ | |
| Chaos test / cancellation test | ⬜ |
บทความส่วนที่ 6 ได้ครอบคลุม patterns ที่จำเป็นสำหรับการนำ OCaml 5 ไปใช้ใน production ระดับองค์กร:
Eio.Switch ควบคุม fiber lifetime อย่างปลอดภัย พร้อม supervision tree patternFun.protect + Switch.on_release ป้องกัน leak ใน concurrent codePatterns เหล่านี้รวมกันทำให้ OCaml 5 พร้อมสำหรับงาน systems ระดับเดียวกับ Rust และ Go โดยคงความแข็งแรงของ type system และ functional abstractions ไว้อย่างครบถ้วน