-40
AGENTS.md
-40
AGENTS.md
···
1
-
# Agent Instructions
2
-
3
-
This project uses **bd** (beads) for issue tracking. Run `bd onboard` to get started.
4
-
5
-
## Quick Reference
6
-
7
-
```bash
8
-
bd ready # Find available work
9
-
bd show <id> # View issue details
10
-
bd update <id> --status in_progress # Claim work
11
-
bd close <id> # Complete work
12
-
bd sync # Sync with git
13
-
```
14
-
15
-
## Landing the Plane (Session Completion)
16
-
17
-
**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds.
18
-
19
-
**MANDATORY WORKFLOW:**
20
-
21
-
1. **File issues for remaining work** - Create issues for anything that needs follow-up
22
-
2. **Run quality gates** (if code changed) - Tests, linters, builds
23
-
3. **Update issue status** - Close finished work, update in-progress items
24
-
4. **PUSH TO REMOTE** - This is MANDATORY:
25
-
```bash
26
-
git pull --rebase
27
-
bd sync
28
-
git push
29
-
git status # MUST show "up to date with origin"
30
-
```
31
-
5. **Clean up** - Clear stashes, prune remote branches
32
-
6. **Verify** - All changes committed AND pushed
33
-
7. **Hand off** - Provide context for next session
34
-
35
-
**CRITICAL RULES:**
36
-
- Work is NOT complete until `git push` succeeds
37
-
- NEVER stop before pushing - that leaves work stranded locally
38
-
- NEVER say "ready to push when you are" - YOU must push
39
-
- If push fails, resolve and retry until it succeeds
40
-
···
+177
README.md
+177
README.md
···
···
1
+
# swim
2
+
3
+
An OCaml 5 implementation of the SWIM (Scalable Weakly-consistent Infection-style Process Group Membership) protocol for cluster membership and failure detection.
4
+
5
+
## Overview
6
+
7
+
This library provides:
8
+
9
+
- **Membership Management**: Automatic discovery and tracking of cluster nodes
10
+
- **Failure Detection**: Identifies unreachable nodes using periodic probes and indirect checks
11
+
- **Gossip Protocol**: Propagates state changes (Alive/Suspect/Dead) across the cluster
12
+
- **Messaging**: Cluster-wide broadcast (gossip-based) and direct point-to-point UDP messaging
13
+
- **Encryption**: Optional AES-256-GCM encryption for all network traffic
14
+
15
+
Built on [Eio](https://github.com/ocaml-multicore/eio) for effect-based concurrency and [Kcas](https://github.com/ocaml-multicore/kcas) for lock-free shared state.
16
+
17
+
## Requirements
18
+
19
+
- OCaml >= 5.1
20
+
- Dune >= 3.20
21
+
22
+
## Installation
23
+
24
+
```bash
25
+
opam install .
26
+
```
27
+
28
+
Or add to your dune-project:
29
+
30
+
```
31
+
(depends (swim (>= 0.1.0)))
32
+
```
33
+
34
+
## Usage
35
+
36
+
### Basic Example
37
+
38
+
```ocaml
39
+
open Swim.Types
40
+
41
+
let config = {
42
+
default_config with
43
+
bind_port = 7946;
44
+
node_name = Some "node-1";
45
+
secret_key = "your-32-byte-secret-key-here!!!"; (* 32 bytes for AES-256 *)
46
+
encryption_enabled = true;
47
+
}
48
+
49
+
let () =
50
+
Eio_main.run @@ fun env ->
51
+
Eio.Switch.run @@ fun sw ->
52
+
let env_wrap = { stdenv = env; sw } in
53
+
match Swim.Cluster.create ~sw ~env:env_wrap ~config with
54
+
| Error `Invalid_key -> failwith "Invalid secret key"
55
+
| Ok cluster ->
56
+
Swim.Cluster.start cluster;
57
+
58
+
(* Join an existing cluster *)
59
+
let seed_nodes = ["192.168.1.10:7946"] in
60
+
(match Swim.Cluster.join cluster ~seed_nodes with
61
+
| Ok () -> Printf.printf "Joined cluster\n"
62
+
| Error `No_seeds_reachable -> Printf.printf "Failed to join\n");
63
+
64
+
(* Send a broadcast message to all nodes *)
65
+
Swim.Cluster.broadcast cluster ~topic:"config" ~payload:"v2";
66
+
67
+
(* Send a direct message to a specific node *)
68
+
let target = node_id_of_string "node-2" in
69
+
Swim.Cluster.send cluster ~target ~topic:"ping" ~payload:"hello";
70
+
71
+
(* Handle incoming messages *)
72
+
Swim.Cluster.on_message cluster (fun sender topic payload ->
73
+
Printf.printf "From %s: [%s] %s\n"
74
+
(node_id_to_string sender.id) topic payload);
75
+
76
+
(* Listen for membership events *)
77
+
Eio.Fiber.fork ~sw (fun () ->
78
+
let stream = Swim.Cluster.events cluster in
79
+
while true do
80
+
match Eio.Stream.take stream with
81
+
| Join node -> Printf.printf "Joined: %s\n" (node_id_to_string node.id)
82
+
| Leave node -> Printf.printf "Left: %s\n" (node_id_to_string node.id)
83
+
| Suspect_event node -> Printf.printf "Suspect: %s\n" (node_id_to_string node.id)
84
+
| Alive_event node -> Printf.printf "Alive: %s\n" (node_id_to_string node.id)
85
+
| Update _ -> ()
86
+
done);
87
+
88
+
Eio.Fiber.await_cancel ()
89
+
```
90
+
91
+
### Configuration Options
92
+
93
+
| Field | Default | Description |
94
+
|-------|---------|-------------|
95
+
| `bind_addr` | "0.0.0.0" | Interface to bind listeners |
96
+
| `bind_port` | 7946 | Port for SWIM protocol |
97
+
| `protocol_interval` | 1.0 | Seconds between probe rounds |
98
+
| `probe_timeout` | 0.5 | Seconds to wait for Ack |
99
+
| `indirect_checks` | 3 | Peers to ask for indirect probes |
100
+
| `secret_key` | (zeros) | 32-byte key for AES-256-GCM |
101
+
| `encryption_enabled` | false | Enable encryption |
102
+
103
+
## Interoperability Testing
104
+
105
+
The library includes interoperability tests with HashiCorp's [memberlist](https://github.com/hashicorp/memberlist) (Go). This verifies protocol compatibility with the reference implementation.
106
+
107
+
### Prerequisites
108
+
109
+
- Go >= 1.19
110
+
- OCaml environment with dune
111
+
112
+
### Running Interop Tests
113
+
114
+
The interop test suite starts a Go memberlist node and an OCaml node, then verifies they can discover each other and exchange messages.
115
+
116
+
```bash
117
+
# Build the OCaml project
118
+
dune build
119
+
120
+
# Build the Go memberlist server
121
+
cd interop && go build -o memberlist-server main.go && cd ..
122
+
123
+
# Run the interop test
124
+
bash test/scripts/test_interop.sh
125
+
126
+
# Run with encryption enabled
127
+
bash test/scripts/test_interop_encrypted.sh
128
+
```
129
+
130
+
### Manual Interop Testing
131
+
132
+
Start the Go node:
133
+
134
+
```bash
135
+
cd interop
136
+
go run main.go -name go-node -bind 127.0.0.1 -port 7946
137
+
```
138
+
139
+
In another terminal, start the OCaml node:
140
+
141
+
```bash
142
+
dune exec swim-interop-test
143
+
```
144
+
145
+
The OCaml node will connect to the Go node and print membership statistics for 30 seconds.
146
+
147
+
### Available Test Scripts
148
+
149
+
| Script | Description |
150
+
|--------|-------------|
151
+
| `test/scripts/test_interop.sh` | Basic interop test |
152
+
| `test/scripts/test_interop_encrypted.sh` | Interop with AES encryption |
153
+
| `test/scripts/test_interop_udp_only.sh` | UDP-only communication test |
154
+
| `test/scripts/test_interop_go_joins.sh` | Go node joining OCaml cluster |
155
+
156
+
### Debug Utilities
157
+
158
+
```bash
159
+
# Test packet encoding/decoding
160
+
dune exec swim-debug-codec
161
+
162
+
# Receive and display incoming SWIM packets
163
+
dune exec swim-debug-recv
164
+
165
+
# Send manual ping to a target node
166
+
dune exec swim-debug-ping
167
+
```
168
+
169
+
## Running Tests
170
+
171
+
```bash
172
+
dune runtest
173
+
```
174
+
175
+
## License
176
+
177
+
ISC License. See [LICENSE](LICENSE) for details.
+8
-5
dune-project
+8
-5
dune-project
···
1
(lang dune 3.20)
2
3
(name swim)
4
5
(generate_opam_files true)
6
7
(source
8
-
(github gdiazlo/swim))
9
10
-
(authors "Guillermo Diaz-Romero <guillermo.diaz@gmail.com>")
11
12
-
(maintainers "Guillermo Diaz-Romero <guillermo.diaz@gmail.com>")
13
14
-
(license MIT)
15
16
-
(documentation https://github.com/gdiazlo/swim)
17
18
(package
19
(name swim)
···
1
(lang dune 3.20)
2
3
(name swim)
4
+
(version 0.1.0)
5
6
(generate_opam_files true)
7
8
(source
9
+
(uri git+https://tangled.org/gdiazlo.tngl.sh/swim))
10
11
+
(authors "Gabriel Diaz")
12
13
+
(maintainers "Gabriel Diaz")
14
15
+
(license ISC)
16
17
+
(homepage https://tangled.org/gdiazlo.tngl.sh/swim)
18
+
(bug_reports https://tangled.org/gdiazlo.tngl.sh/swim/issues)
19
+
(documentation https://tangled.org/gdiazlo.tngl.sh/swim)
20
21
(package
22
(name swim)
+8
-59
lib/buffer_pool.ml
+8
-59
lib/buffer_pool.ml
···
1
-
(** Lock-free buffer pool using Kcas and Eio.
2
-
3
-
Provides pre-allocated buffers for zero-copy I/O operations. Uses
4
-
Kcas_data.Queue for lock-free buffer storage and Eio.Semaphore for blocking
5
-
acquire when pool is exhausted. *)
6
-
7
-
type t = {
8
-
buffers : Cstruct.t Kcas_data.Queue.t;
9
-
buf_size : int;
10
-
total : int;
11
-
semaphore : Eio.Semaphore.t;
12
-
}
13
14
let create ~size ~count =
15
-
let buffers = Kcas_data.Queue.create () in
16
for _ = 1 to count do
17
-
Kcas.Xt.commit
18
-
{
19
-
tx =
20
-
(fun ~xt -> Kcas_data.Queue.Xt.add ~xt (Cstruct.create size) buffers);
21
-
}
22
done;
23
-
{
24
-
buffers;
25
-
buf_size = size;
26
-
total = count;
27
-
semaphore = Eio.Semaphore.make count;
28
-
}
29
30
-
let acquire t =
31
-
Eio.Semaphore.acquire t.semaphore;
32
-
let buf_opt =
33
-
Kcas.Xt.commit
34
-
{ tx = (fun ~xt -> Kcas_data.Queue.Xt.take_opt ~xt t.buffers) }
35
-
in
36
-
match buf_opt with
37
-
| Some buf -> buf
38
-
| None ->
39
-
(* Should not happen if semaphore is properly synchronized,
40
-
but handle gracefully by allocating a new buffer *)
41
-
Cstruct.create t.buf_size
42
-
43
-
let try_acquire t =
44
-
(* Check if semaphore has available permits without blocking *)
45
-
if Eio.Semaphore.get_value t.semaphore > 0 then begin
46
-
(* Race condition possible here - another fiber might acquire between
47
-
get_value and acquire. In that case, acquire will block briefly.
48
-
For truly non-blocking behavior, we'd need atomic CAS on semaphore. *)
49
-
Eio.Semaphore.acquire t.semaphore;
50
-
let buf_opt =
51
-
Kcas.Xt.commit
52
-
{ tx = (fun ~xt -> Kcas_data.Queue.Xt.take_opt ~xt t.buffers) }
53
-
in
54
-
match buf_opt with
55
-
| Some buf -> Some buf
56
-
| None -> Some (Cstruct.create t.buf_size)
57
-
end
58
-
else None
59
-
60
-
let release t buf =
61
-
Kcas.Xt.commit { tx = (fun ~xt -> Kcas_data.Queue.Xt.add ~xt buf t.buffers) };
62
-
Eio.Semaphore.release t.semaphore
63
64
let with_buffer t f =
65
let buf = acquire t in
66
Fun.protect ~finally:(fun () -> release t buf) (fun () -> f buf)
67
68
-
let available t = Eio.Semaphore.get_value t.semaphore
69
-
let total t = t.total
70
let size t = t.buf_size
···
1
+
type t = { pool : Cstruct.t Eio.Stream.t; buf_size : int; capacity : int }
2
3
let create ~size ~count =
4
+
let pool = Eio.Stream.create count in
5
for _ = 1 to count do
6
+
Eio.Stream.add pool (Cstruct.create size)
7
done;
8
+
{ pool; buf_size = size; capacity = count }
9
10
+
let acquire t = Eio.Stream.take t.pool
11
+
let release t buf = Eio.Stream.add t.pool buf
12
13
let with_buffer t f =
14
let buf = acquire t in
15
Fun.protect ~finally:(fun () -> release t buf) (fun () -> f buf)
16
17
+
let available t = Eio.Stream.length t.pool
18
+
let total t = t.capacity
19
let size t = t.buf_size
-1
lib/buffer_pool.mli
-1
lib/buffer_pool.mli
+8
-7
swim.opam
+8
-7
swim.opam
···
1
# This file is generated by dune, edit dune-project instead
2
opam-version: "2.0"
3
synopsis:
4
"SWIM protocol library for cluster membership and failure detection"
5
description:
6
"Production-ready SWIM (Scalable Weakly-consistent Infection-style Process Group Membership) protocol library in OCaml 5 for cluster membership, failure detection, and lightweight pub/sub messaging. Features lock-free coordination via kcas, zero-copy buffer management, and AES-256-GCM encryption."
7
-
maintainer: ["Guillermo Diaz-Romero <guillermo.diaz@gmail.com>"]
8
-
authors: ["Guillermo Diaz-Romero <guillermo.diaz@gmail.com>"]
9
-
license: "MIT"
10
tags: [
11
"swim" "cluster" "membership" "gossip" "failure detection" "ocaml5" "eio"
12
]
13
-
homepage: "https://github.com/gdiazlo/swim"
14
-
doc: "https://github.com/gdiazlo/swim"
15
-
bug-reports: "https://github.com/gdiazlo/swim/issues"
16
depends: [
17
"ocaml" {>= "5.1"}
18
"dune" {>= "3.20" & >= "3.20"}
···
46
"@doc" {with-doc}
47
]
48
]
49
-
dev-repo: "git+https://github.com/gdiazlo/swim.git"
50
x-maintenance-intent: ["(latest)"]
···
1
# This file is generated by dune, edit dune-project instead
2
opam-version: "2.0"
3
+
version: "0.1.0"
4
synopsis:
5
"SWIM protocol library for cluster membership and failure detection"
6
description:
7
"Production-ready SWIM (Scalable Weakly-consistent Infection-style Process Group Membership) protocol library in OCaml 5 for cluster membership, failure detection, and lightweight pub/sub messaging. Features lock-free coordination via kcas, zero-copy buffer management, and AES-256-GCM encryption."
8
+
maintainer: ["Gabriel Diaz"]
9
+
authors: ["Gabriel Diaz"]
10
+
license: "ISC"
11
tags: [
12
"swim" "cluster" "membership" "gossip" "failure detection" "ocaml5" "eio"
13
]
14
+
homepage: "https://tangled.org/gdiazlo.tngl.sh/swim"
15
+
doc: "https://tangled.org/gdiazlo.tngl.sh/swim"
16
+
bug-reports: "https://tangled.org/gdiazlo.tngl.sh/swim/issues"
17
depends: [
18
"ocaml" {>= "5.1"}
19
"dune" {>= "3.20" & >= "3.20"}
···
47
"@doc" {with-doc}
48
]
49
]
50
+
dev-repo: "git+https://tangled.org/gdiazlo.tngl.sh/swim"
51
x-maintenance-intent: ["(latest)"]
+21
test/scripts/test_interop.sh
+21
test/scripts/test_interop.sh
···
···
1
+
#!/bin/bash
2
+
set -e
3
+
4
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+
REPO_ROOT="$SCRIPT_DIR/../.."
6
+
7
+
echo "Starting Go memberlist server WITHOUT encryption..."
8
+
cd "$REPO_ROOT/interop"
9
+
./memberlist-server -name go-node -port 7946 &
10
+
GO_PID=$!
11
+
sleep 2
12
+
13
+
echo "Starting OCaml SWIM client..."
14
+
cd "$REPO_ROOT"
15
+
timeout 25 ./_build/default/bin/interop_test.exe || true
16
+
17
+
echo "Killing Go server..."
18
+
kill $GO_PID 2>/dev/null || true
19
+
wait $GO_PID 2>/dev/null || true
20
+
21
+
echo "Done"
+24
test/scripts/test_interop_encrypted.sh
+24
test/scripts/test_interop_encrypted.sh
···
···
1
+
#!/bin/bash
2
+
set -e
3
+
4
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+
REPO_ROOT="$SCRIPT_DIR/../.."
6
+
7
+
# Test key: 16 bytes (0x00-0x0f) in hex
8
+
TEST_KEY="000102030405060708090a0b0c0d0e0f"
9
+
10
+
echo "Starting Go memberlist server WITH encryption..."
11
+
cd "$REPO_ROOT/interop"
12
+
./memberlist-server -name go-node -port 7946 -key "$TEST_KEY" &
13
+
GO_PID=$!
14
+
sleep 2
15
+
16
+
echo "Starting OCaml SWIM client WITH encryption..."
17
+
cd "$REPO_ROOT"
18
+
timeout 25 ./_build/default/bin/interop_test.exe --encrypt || true
19
+
20
+
echo "Killing Go server..."
21
+
kill $GO_PID 2>/dev/null || true
22
+
wait $GO_PID 2>/dev/null || true
23
+
24
+
echo "Done"
+29
test/scripts/test_interop_go_joins.sh
+29
test/scripts/test_interop_go_joins.sh
···
···
1
+
#!/bin/bash
2
+
set -e
3
+
4
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+
REPO_ROOT="$SCRIPT_DIR/../.."
6
+
7
+
# Test where Go node joins to OCaml node (reverse direction)
8
+
9
+
echo "Starting OCaml SWIM server..."
10
+
cd "$REPO_ROOT"
11
+
timeout 25 ./_build/default/bin/interop_test.exe &
12
+
OCAML_PID=$!
13
+
sleep 2
14
+
15
+
echo "Starting Go memberlist and joining to OCaml..."
16
+
cd "$REPO_ROOT/interop"
17
+
./memberlist-server -name go-node -port 7946 -join "127.0.0.1:7947" &
18
+
GO_PID=$!
19
+
20
+
# Let them communicate for a while
21
+
sleep 15
22
+
23
+
echo "Killing processes..."
24
+
kill $GO_PID 2>/dev/null || true
25
+
kill $OCAML_PID 2>/dev/null || true
26
+
wait $GO_PID 2>/dev/null || true
27
+
wait $OCAML_PID 2>/dev/null || true
28
+
29
+
echo "Done"
+31
test/scripts/test_interop_udp_only.sh
+31
test/scripts/test_interop_udp_only.sh
···
···
1
+
#!/bin/bash
2
+
set -e
3
+
4
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+
REPO_ROOT="$SCRIPT_DIR/../.."
6
+
7
+
# Test UDP-only communication (no TCP join)
8
+
# Both nodes start independently, OCaml adds Go to its membership
9
+
# They should then be able to gossip via UDP
10
+
11
+
echo "Starting Go memberlist server (no join)..."
12
+
cd "$REPO_ROOT/interop"
13
+
./memberlist-server -name go-node -port 7946 &
14
+
GO_PID=$!
15
+
sleep 2
16
+
17
+
echo "Starting OCaml SWIM client (adds Go node manually)..."
18
+
cd "$REPO_ROOT"
19
+
timeout 20 ./_build/default/bin/interop_test.exe &
20
+
OCAML_PID=$!
21
+
22
+
# Let them communicate
23
+
sleep 15
24
+
25
+
echo "Killing processes..."
26
+
kill $GO_PID 2>/dev/null || true
27
+
kill $OCAML_PID 2>/dev/null || true
28
+
wait $GO_PID 2>/dev/null || true
29
+
wait $OCAML_PID 2>/dev/null || true
30
+
31
+
echo "Done"
-18
test_interop.sh
-18
test_interop.sh
···
1
-
#!/bin/bash
2
-
set -e
3
-
4
-
echo "Starting Go memberlist server WITHOUT encryption..."
5
-
cd /home/gdiazlo/data/src/swim/interop
6
-
./memberlist-server -name go-node -port 7946 &
7
-
GO_PID=$!
8
-
sleep 2
9
-
10
-
echo "Starting OCaml SWIM client..."
11
-
cd /home/gdiazlo/data/src/swim
12
-
timeout 25 ./_build/default/bin/interop_test.exe || true
13
-
14
-
echo "Killing Go server..."
15
-
kill $GO_PID 2>/dev/null || true
16
-
wait $GO_PID 2>/dev/null || true
17
-
18
-
echo "Done"
···
-21
test_interop_encrypted.sh
-21
test_interop_encrypted.sh
···
1
-
#!/bin/bash
2
-
set -e
3
-
4
-
# Test key: 16 bytes (0x00-0x0f) in hex
5
-
TEST_KEY="000102030405060708090a0b0c0d0e0f"
6
-
7
-
echo "Starting Go memberlist server WITH encryption..."
8
-
cd /home/gdiazlo/data/src/swim/interop
9
-
./memberlist-server -name go-node -port 7946 -key "$TEST_KEY" &
10
-
GO_PID=$!
11
-
sleep 2
12
-
13
-
echo "Starting OCaml SWIM client WITH encryption..."
14
-
cd /home/gdiazlo/data/src/swim
15
-
timeout 25 ./_build/default/bin/interop_test.exe --encrypt || true
16
-
17
-
echo "Killing Go server..."
18
-
kill $GO_PID 2>/dev/null || true
19
-
wait $GO_PID 2>/dev/null || true
20
-
21
-
echo "Done"
···
-26
test_interop_go_joins.sh
-26
test_interop_go_joins.sh
···
1
-
#!/bin/bash
2
-
set -e
3
-
4
-
# Test where Go node joins to OCaml node (reverse direction)
5
-
6
-
echo "Starting OCaml SWIM server..."
7
-
cd /home/gdiazlo/data/src/swim
8
-
timeout 25 ./_build/default/bin/interop_test.exe &
9
-
OCAML_PID=$!
10
-
sleep 2
11
-
12
-
echo "Starting Go memberlist and joining to OCaml..."
13
-
cd /home/gdiazlo/data/src/swim/interop
14
-
./memberlist-server -name go-node -port 7946 -join "127.0.0.1:7947" &
15
-
GO_PID=$!
16
-
17
-
# Let them communicate for a while
18
-
sleep 15
19
-
20
-
echo "Killing processes..."
21
-
kill $GO_PID 2>/dev/null || true
22
-
kill $OCAML_PID 2>/dev/null || true
23
-
wait $GO_PID 2>/dev/null || true
24
-
wait $OCAML_PID 2>/dev/null || true
25
-
26
-
echo "Done"
···
-28
test_interop_udp_only.sh
-28
test_interop_udp_only.sh
···
1
-
#!/bin/bash
2
-
set -e
3
-
4
-
# Test UDP-only communication (no TCP join)
5
-
# Both nodes start independently, OCaml adds Go to its membership
6
-
# They should then be able to gossip via UDP
7
-
8
-
echo "Starting Go memberlist server (no join)..."
9
-
cd /home/gdiazlo/data/src/swim/interop
10
-
./memberlist-server -name go-node -port 7946 &
11
-
GO_PID=$!
12
-
sleep 2
13
-
14
-
echo "Starting OCaml SWIM client (adds Go node manually)..."
15
-
cd /home/gdiazlo/data/src/swim
16
-
timeout 20 ./_build/default/bin/interop_test.exe &
17
-
OCAML_PID=$!
18
-
19
-
# Let them communicate
20
-
sleep 15
21
-
22
-
echo "Killing processes..."
23
-
kill $GO_PID 2>/dev/null || true
24
-
kill $OCAML_PID 2>/dev/null || true
25
-
wait $GO_PID 2>/dev/null || true
26
-
wait $OCAML_PID 2>/dev/null || true
27
-
28
-
echo "Done"
···