-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathmemory_bank.ex
More file actions
126 lines (98 loc) · 3.62 KB
/
Copy pathmemory_bank.ex
File metadata and controls
126 lines (98 loc) · 3.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
defmodule MemoryBank do
use GenServer
# Respawn policy: wait @backoff_ms between respawn attempts, and give up
# after @max_failures consecutive quick failures. A port that ran for at
# least @success_threshold_ms before exiting resets the failure count so
# a normal long-lived python that dies once does not count against it.
@backoff_ms 5000
@max_failures 10
@success_threshold_ms 30_000
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: :memory_bank)
end
def init(_opts) do
settings_path = Path.join(__DIR__, "settings.txt")
content = read_file(settings_path)
settings = case Jason.decode(content) do
{:ok, decoded} -> decoded
{:error, _} -> %{}
end
homepath = Map.get(settings, "homepath", "/app/")
python_script = Path.join(__DIR__, "tools/memory_bank.py")
port_num = Map.get(settings, "memory_bank_port", 9500)
state = %{
os_port: nil,
spawn_time: nil,
fail_count: 0,
halted: false,
homepath: homepath,
python_script: python_script,
port_num: port_num
}
{:ok, spawn_port(state)}
end
# ===== PORT LIFECYCLE =====
defp spawn_port(state) do
python = System.find_executable("python3") || "python3"
port = Port.open({:spawn_executable, python}, [
:binary, :use_stdio, {:line, 65536}, :exit_status,
{:args, [state.python_script, state.homepath, Integer.to_string(state.port_num)]}
])
%{state | os_port: port, spawn_time: System.monotonic_time(:millisecond)}
end
defp handle_port_exit(state) do
age = System.monotonic_time(:millisecond) - (state.spawn_time || 0)
new_fail = if age >= @success_threshold_ms, do: 0, else: state.fail_count + 1
if new_fail >= @max_failures do
IO.puts("MemoryBank: halting respawns after " <> Integer.to_string(new_fail) <> " consecutive quick failures. Check python3 " <> state.python_script <> " and restart the service.")
write_halt_flag(state.homepath, "memory_bank", state.python_script, new_fail)
%{state | os_port: nil, fail_count: new_fail, halted: true}
else
IO.puts("MemoryBank: python exited after " <> Integer.to_string(age) <> "ms (fail_count=" <> Integer.to_string(new_fail) <> "). Respawning in " <> Integer.to_string(@backoff_ms) <> "ms.")
Process.send_after(self(), :respawn, @backoff_ms)
%{state | os_port: nil, fail_count: new_fail}
end
end
# ===== RECEIVING FROM PYTHON (stdout) =====
def handle_info({port, {:data, {:eol, _line}}}, state) when port == state.os_port do
{:noreply, state}
end
def handle_info({port, {:exit_status, _status}}, state) when port == state.os_port do
{:noreply, handle_port_exit(state)}
end
def handle_info({:EXIT, port, _reason}, state) when port == state.os_port do
{:noreply, handle_port_exit(state)}
end
def handle_info(:respawn, state) do
if state.halted do
{:noreply, state}
else
{:noreply, spawn_port(state)}
end
end
def handle_info(_msg, state) do
{:noreply, state}
end
# ===== RECEIVING FROM OTHER NODES =====
def handle_cast({:core_message, _from, _data}, state) do
{:noreply, state}
end
def handle_cast({:master_message, _data}, state) do
{:noreply, state}
end
# ===== UTILITY =====
# Surface a permanent halt to the UI/health layer via the flag directory,
# since the GenServer stays alive and the supervisor will not restart it.
defp write_halt_flag(homepath, service, script, fails) do
flags_dir = homepath <> "threads/flags/"
File.mkdir_p(flags_dir)
path = flags_dir <> "flag_halt_" <> service <> ".txt"
File.write(path, "type: system_observation\nsource: " <> service <> "\ntopic: service halted\n\n" <> service <> " halted after " <> Integer.to_string(fails) <> " quick failures. Check python3 " <> script <> " and restart the service.\n")
end
defp read_file(path) do
case File.read(path) do
{:ok, content} -> content
{:error, _} -> ""
end
end
end