okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

Aly Raffauffix(runtime): unify extension reload and recover failed replacement840828d

main
10.1 KiB263 linesraw
1//// Kernel ownership and timed-out execution state need nonflaky runtime-level probes.
2
3import albedo/daemon/conversation
4import albedo/daemon/session
5import albedo/daemon/store
6import albedo/harness/extension
7import albedo/harness/extension/selection
8import albedo/harness/extensions/python/kernel as python
9import albedo/harness/extensions/python/link
10import albedo/harness/runtime
11import albedo/harness/runtime/state as runtime_state
12import gleam/erlang/process
13import gleam/int
14import gleam/list
15import gleam/option.{None, Some}
16import gleam/string
17import gleeunit/should
18import harness/session_fixture
19
20@external(erlang, "albedo_runtime_test_support", "kernel_memory")
21fn kernel_memory(session: runtime.Session) -> Int
22
23@external(erlang, "albedo_runtime_test_support", "temporary_workspace")
24fn temporary_workspace() -> String
25
26@external(erlang, "albedo_runtime_test_support", "cleanup_workspace")
27fn cleanup_workspace(path: String) -> Nil
28
29@external(erlang, "albedo_copy_test_support", "captured_words")
30fn captured_words(term: a) -> Int
31
32/// Every http request and every turn's worker copies the runtime handle and
33/// a session's selection into a fresh process, so neither may carry the
34/// installed extensions: the handle shares them, a selection points at them.
35pub fn runtime_handle_and_selection_carry_no_installed_copy_test() -> Nil {
36  let workspace = temporary_workspace()
37  let assert Ok(host) = runtime.start(":memory:")
38  session_fixture.initialise(host)
39  session_fixture.create(host, "handle", workspace)
40  let assert Ok(_) = runtime.open_session(host, "handle", workspace)
41  let baseline = captured_words(Nil)
42  let installed = runtime.installed(host)
43  let assert Ok(enabled) = runtime.global(host)
44  // Measured while the runtime lives: its stop releases the shared value,
45  // which lands a copy on every heap still holding it.
46  let handle_words = captured_words(host)
47  let installed_words = captured_words(installed)
48  let enabled_words = captured_words(enabled)
49  runtime.stop(host)
50  cleanup_workspace(workspace)
51  should.be_true(list.length(installed) > 10)
52  should.be_true(list.length(enabled) > 10)
53  // The handle: a subject, a store, and a shared reference.
54  should.be_true(handle_words < baseline + 256)
55  installed_words |> should.equal(baseline)
56  // A session's selection: one cons cell per enabled extension.
57  should.be_true(enabled_words < baseline + 256)
58}
59
60pub fn kernel_host_ownership_does_not_grow_with_the_swarm_test() -> Nil {
61  let workspace = temporary_workspace()
62  let assert Ok(host) = runtime.start_with_extensions(":memory:", [])
63  session_fixture.initialise(host)
64  session_fixture.create(host, "first", workspace)
65  let assert Ok(first) = runtime.open_session(host, "first", workspace)
66  let baseline = kernel_memory(first)
67  // A hundred dormant compositions, not a hundred OS processes. None belongs
68  // in another kernel's RPC closure or in the worker that boots that kernel.
69  list.repeat(Nil, 100)
70  |> list.index_map(fn(_, n) {
71    let id = "dormant-" <> int.to_string(n)
72    session_fixture.create(host, id, workspace)
73    let assert Ok(_) = runtime.peek_commands(host, id, workspace)
74    let assert Ok(observed) = runtime.observe_loaded(host, id)
75    observed.kernel |> should.equal(None)
76    observed.phase |> should.equal("none")
77  })
78  session_fixture.create(host, "last", workspace)
79  let assert Ok(last) = runtime.open_session(host, "last", workspace)
80  let opened = kernel_memory(last)
81  let assert Ok(Some(refreshed)) =
82    runtime.reload_desired(host, "last", workspace)
83  let rebound = kernel_memory(refreshed)
84  runtime.stop(host)
85  cleanup_workspace(workspace)
86  // Allocator size classes may differ; sibling state must not accumulate.
87  should.be_true(opened <= baseline * 2 + 4096)
88  should.be_true(rebound <= baseline * 2 + 4096)
89}
90
91pub fn hard_timeout_requires_explicit_reset_test() -> Nil {
92  let assert Ok(host) = runtime.start(":memory:")
93  session_fixture.initialise(host)
94  session_fixture.create(host, "a", "/tmp")
95  let assert Ok(session) = runtime.open_session(host, "a", "/tmp")
96  // SIGINT is ignored before the timed cell starts: in the same cell, a
97  // deadline landing ahead of the signal.signal line under load is a
98  // graceful interrupt instead.
99  let assert Ok(_) =
100    runtime.execute(
101      host,
102      session,
103      "import signal\nsignal.signal(signal.SIGINT, signal.SIG_IGN)",
104      10_000,
105    )
106  let assert Ok(execution) =
107    runtime.execute(host, session, "while True: pass", 100)
108  execution.result |> should.equal(Error(python.Lost))
109  runtime.open_session(host, "a", "/tmp") |> should.equal(Error(python.Lost))
110  runtime.reset_session(host, "a")
111  let assert Ok(_) = runtime.open_session(host, "a", "/tmp")
112  runtime.stop(host)
113}
114
115@external(erlang, "albedo_runtime_test_support", "catalog_does_not_block")
116fn catalog_does_not_block(
117  runtime: runtime.Runtime,
118  home: String,
119  id: String,
120) -> #(Bool, Bool)
121
122pub fn catalog_store_wait_does_not_block_runtime_observations_test() -> Nil {
123  let home = temporary_workspace()
124  let assert Ok(host) = runtime.start(":memory:")
125  session_fixture.initialise(host)
126  session_fixture.create(host, "catalog", home)
127  let #(responsive, completed) = catalog_does_not_block(host, home, "catalog")
128  runtime.stop(host)
129  cleanup_workspace(home)
130  responsive |> should.be_true
131  completed |> should.be_true
132}
133
134@external(erlang, "albedo_runtime_test_support", "catalog_forget_while_blocked")
135fn catalog_forget_while_blocked(
136  runtime: runtime.Runtime,
137  home: String,
138  id: String,
139) -> Result(runtime.CatalogObservation, String)
140
141pub fn stale_catalog_retries_against_current_composition_test() -> Nil {
142  let home = temporary_workspace()
143  let assert Ok(host) = runtime.start_with_extensions(":memory:", [])
144  session_fixture.initialise(host)
145  session_fixture.create(host, "catalog", home)
146  let assert Ok(_) = runtime.peek_commands(host, "catalog", home)
147  let assert Ok(initial) = runtime.observe_catalog(host, home, "catalog")
148  should.be_true(initial.loaded_revision != None)
149  let assert Ok(current) = catalog_forget_while_blocked(host, home, "catalog")
150  current.loaded_revision |> should.equal(None)
151  runtime.loaded_sessions(host) |> should.equal([])
152  let assert Ok(fresh) = runtime.observe_catalog(host, home, "catalog")
153  fresh.loaded_revision |> should.equal(None)
154  runtime.stop(host)
155  cleanup_workspace(home)
156}
157
158@external(erlang, "albedo_runtime_test_support", "hold_preparation")
159fn hold_preparation(
160  parent: process.Pid,
161  workspace: String,
162) -> Result(String, String)
163
164@external(erlang, "albedo_runtime_test_support", "capture_during_preparation")
165fn capture_during_preparation(
166  host: runtime.Runtime,
167  reader: session.Session,
168  requests: List(#(String, String)),
169) -> #(Result(session.Capture, String), Bool)
170
171pub fn blocked_preparations_leave_session_capture_responsive_test() -> Nil {
172  let home = temporary_workspace()
173  let parent = process.self()
174  let held =
175    extension.Extension(
176      name: "held-context",
177      description: "context loading controlled by the test's release barrier",
178      requires: [],
179      plugins: [
180        extension.ContextPlugin(fn(workspace) {
181          hold_preparation(parent, workspace)
182        }),
183      ],
184      initialise: fn(ledger) { store.query(ledger, link.apply) },
185    )
186  let assert Ok(host) = runtime.start_with_extensions(":memory:", [held])
187  session_fixture.initialise(host)
188  let requests =
189    list.repeat(Nil, 8)
190    |> list.index_map(fn(_, index) {
191      let id = "preparing-" <> int.to_string(index)
192      let workspace = temporary_workspace()
193      session_fixture.create(host, id, workspace)
194      // Keep the blocking loader selected regardless of shared global defaults.
195      let assert Ok(_) =
196        selection.record_selected(
197          runtime.ledger(host),
198          id,
199          selection.SetSession(held.name, True),
200          [],
201          [held],
202          [held],
203        )
204      #(id, workspace)
205    })
206  session_fixture.create(host, "reader", home)
207  let assert Ok(info) = conversation.get(runtime.ledger(host), "reader")
208  let assert Ok(reader) = session.start(host, info, home)
209  let #(outcome, completed) = capture_during_preparation(host, reader, requests)
210  session.close(reader)
211  runtime.stop(host)
212  list.each(requests, fn(request) { cleanup_workspace(request.1) })
213  cleanup_workspace(home)
214  let assert Ok(captured) = outcome
215  captured.info.id |> should.equal("reader")
216  completed |> should.be_true
217}
218
219/// SQLite publication failure can only be placed precisely with an injected
220/// trigger. An ordinary daemon request cannot target the post-shutdown gap.
221pub fn failed_publication_never_restores_a_dead_kernel_test() -> Nil {
222  let workspace = temporary_workspace()
223  let assert Ok(host) =
224    runtime.start_with_extensions(":memory:", [
225      extension.python_module("module", "fixture", "run", "", []),
226    ])
227  session_fixture.initialise(host)
228  session_fixture.create(host, "publication", workspace)
229  let assert Ok(old) = runtime.open_session(host, "publication", workspace)
230  let ledger = runtime.ledger(host)
231  let assert Ok(_) =
232    store.query(ledger, fn(db) {
233      store.exec(
234        db,
235        "CREATE TRIGGER refuse_publication BEFORE INSERT ON kernel_links BEGIN SELECT RAISE(FAIL, 'publication refused'); END;",
236      )
237    })
238  let assert runtime_state.ApplyFailed(retained, reason) =
239    runtime.apply_change(
240      host,
241      "publication",
242      workspace,
243      selection.SetSession("module", False),
244    )
245  retained |> should.equal(None)
246  runtime.alive(old) |> should.be_false
247  string.contains(reason, "publication refused") |> should.be_true
248  let assert Ok(loaded) = runtime.observe_loaded(host, "publication")
249  loaded.loaded_revision |> should.equal(None)
250  loaded.kernel |> should.equal(None)
251  let assert Ok(chosen) = selection.overrides(ledger, "publication")
252  list.contains(chosen, #("module", False)) |> should.be_true
253  let assert Ok(stages) = link.stages(ledger)
254  stages |> should.equal([])
255  let assert Ok(_) =
256    store.query(ledger, fn(db) {
257      store.exec(db, "DROP TRIGGER refuse_publication")
258    })
259  let assert Ok(fresh) = runtime.open_session(host, "publication", workspace)
260  runtime.alive(fresh) |> should.be_true
261  runtime.stop(host)
262  cleanup_workspace(workspace)
263}