okami.mom/albedo

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

dawnkernel: a job_stop frame lets /jobs stop remote background jobs57a82be

main
8.2 KiB200 linesraw
1"""The kernel half of the session layer, exercised frame by frame.
2
3Sequence, acknowledgement, and replay rules decide whether a reconnect loses
4or repeats a message, and E2E cannot choose where a connection drops. These
5tests drive albedo_link directly: the outbox's coalescing and bounds, replayed
6daemon frames, token checks, newest-attach-wins, and resending what the daemon
7never acknowledged.
8"""
9
10from pathlib import Path
11import os
12import shutil
13import socket
14import sys
15import tempfile
16import threading
17import unittest
18from unittest.mock import patch
19
20sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "priv" / "python"))
21import albedo_link  # noqa: E402
22
23
24class OutboxTest(unittest.TestCase):
25    def test_ack_drops_everything_up_to_it(self):
26        outbox = albedo_link.Outbox()
27        for n in range(4):
28            outbox.add({"type": "call", "id": str(n)})
29        outbox.ack(2)
30        self.assertEqual([seq for seq, _ in outbox.pending()], [3, 4])
31
32    def test_mirrors_and_traces_keep_only_their_newest_copy(self):
33        outbox = albedo_link.Outbox()
34        outbox.add({"type": "mirror", "handle": "a", "tail": "1"})
35        outbox.add({"type": "mirror", "handle": "b", "tail": "1"})
36        outbox.add({"type": "done", "id": "c1"})
37        outbox.add({"type": "mirror", "handle": "a", "tail": "2"})
38        frames = [data for _, data in outbox.pending()]
39        self.assertEqual(len(frames), 3)
40        self.assertIn(b'"tail":"2"', frames[-1])
41        self.assertNotIn(b'"handle":"a","tail":"1"', b"".join(frames))
42
43    def test_the_bound_sheds_coalescing_frames_before_deliveries(self):
44        outbox = albedo_link.Outbox(frames=3)
45        outbox.add({"type": "done", "id": "c1"})
46        outbox.add({"type": "mirror", "handle": "a"})
47        outbox.add({"type": "call", "id": "x"})
48        outbox.add({"type": "job", "id": "j"})
49        kinds = [data for _, data in outbox.pending()]
50        self.assertEqual(len(kinds), 3)
51        self.assertFalse(any(b"mirror" in data for data in kinds))
52        self.assertEqual(outbox.dropped, 0)
53        outbox.add({"type": "done", "id": "c2"})
54        self.assertEqual(outbox.dropped, 1)
55        self.assertNotIn(b'"c1"', b"".join(data for _, data in outbox.pending()))
56
57    def test_the_bound_sheds_the_oldest_coalescing_frame_first(self):
58        outbox = albedo_link.Outbox(frames=3)
59        outbox.add({"type": "mirror", "handle": "a"})
60        outbox.add({"type": "mirror", "handle": "b"})
61        outbox.add({"type": "done", "id": "c1"})
62        outbox.add({"type": "mirror", "handle": "c"})
63        frames = b"".join(data for _, data in outbox.pending())
64        self.assertNotIn(b'"handle":"a"', frames)
65        self.assertIn(b'"handle":"b"', frames)
66        self.assertIn(b'"handle":"c"', frames)
67        self.assertEqual(outbox.dropped, 0)
68
69    def test_a_replayed_daemon_frame_is_not_applied_twice(self):
70        inbound = albedo_link.Inbound()
71        self.assertEqual(
72            [inbound.accept(seq) for seq in (1, 2, 2, 1, 3)],
73            [True, True, False, False, True],
74        )
75
76    def test_job_book_follows_started_and_proven_gone_jobs(self):
77        book = albedo_link.JobBook()
78        book.observe({"type": "job_start", "id": "a", "pgid": 4242})
79        book.observe({"type": "job_start", "id": "b", "pgid": 4243})
80        book.observe({"type": "job", "id": "a", "cleanup": {"gone": False}})
81        book.observe({"type": "job", "id": "b", "cleanup": {"gone": True}})
82        book.observe({"type": "jobs", "live": 2})
83        self.assertEqual(sorted(book.started), ["a"])
84        self.assertEqual(book.live(), 3)
85
86    def test_job_book_keeps_the_remote_jobs_a_frame_lists(self):
87        book = albedo_link.JobBook()
88        item = {"id": "r", "command": "sleep 9", "service": False, "started": 1}
89        book.observe({"type": "jobs", "live": 1, "awaited": 1, "items": [item]})
90        self.assertEqual(book.external_jobs, [item])
91        book.observe({"type": "jobs", "live": 0, "awaited": 0})
92        self.assertEqual(book.external_jobs, [])
93
94
95class SocketLinkTest(unittest.TestCase):
96    def setUp(self):
97        self.run_dir = tempfile.mkdtemp(prefix="link-")
98        self.received = []
99        self.link = albedo_link.SocketLink(
100            self.run_dir,
101            "secret",
102            lambda: {"pid": os.getpid()},
103            bundle="bundle",
104            grace=60,
105        )
106        threading.Thread(target=self.serve, daemon=True).start()
107
108    def serve(self):
109        try:
110            self.link.serve(self.received.append)
111        except OSError:
112            pass  # the listener closed in tearDown
113
114    def tearDown(self):
115        self.link.listener.close()
116        shutil.rmtree(self.run_dir, ignore_errors=True)
117
118    def attach(self, token="secret", ack=0):
119        connection = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
120        connection.settimeout(5)
121        connection.connect(os.path.join(self.run_dir, albedo_link.SOCKET))
122        self.addCleanup(connection.close)
123        attach = {"attach": {"token": token, "ack": ack, "grace": 30}}
124        albedo_link.write_frame(connection.send, albedo_link.encode(attach))
125        return connection, albedo_link.read_frame(connection.recv)
126
127    def frames(self, connection, count):
128        return [albedo_link.read_frame(connection.recv) for _ in range(count)]
129
130    def test_a_wrong_token_is_refused(self):
131        _, answer = self.attach(token="guess")
132        self.assertEqual(answer, {"refused": "wrong token"})
133        self.assertFalse(self.link.attached())
134
135    def test_reattach_resends_only_what_was_not_acknowledged(self):
136        first, hello = self.attach()
137        self.assertEqual((hello["hello"]["epoch"], hello["hello"]["ack"]), (1, 0))
138        self.assertEqual(self.link.grace, 30)
139        for n in range(3):
140            self.link.send({"type": "call", "id": str(n)})
141        self.assertEqual([f["seq"] for f in self.frames(first, 3)], [1, 2, 3])
142        albedo_link.write_frame(
143            first.send,
144            albedo_link.encode(
145                {"seq": 1, "ack": 1, "frame": {"type": "release", "handle": "h"}}
146            ),
147        )
148        self.assertEqual(albedo_link.read_frame(first.recv), {"ack": 1})
149        second, hello = self.attach(ack=2)
150        # Newest attach wins: the first connection is cut off.
151        self.assertEqual(first.recv(1), b"")
152        self.assertEqual((hello["hello"]["epoch"], hello["hello"]["ack"]), (2, 1))
153        (resent,) = self.frames(second, 1)
154        self.assertEqual((resent["seq"], resent["frame"]["id"]), (3, "2"))
155        # The daemon replays seq 1 after its restart: it is not delivered again.
156        replay = {"seq": 1, "ack": 3, "frame": {"type": "release", "handle": "h"}}
157        albedo_link.write_frame(second.send, albedo_link.encode(replay))
158        self.assertEqual(albedo_link.read_frame(second.recv), {"ack": 1})
159        self.assertEqual(len(self.received), 1)
160        self.assertEqual(self.link.outbox.pending(), [])
161
162    def test_frames_sent_while_detached_wait_for_the_next_attach(self):
163        self.link.send({"type": "done", "id": "c1"})
164        self.assertGreater(self.link.idle_for(), 0)
165        connection, _ = self.attach()
166        (done,) = self.frames(connection, 1)
167        self.assertEqual(done["frame"], {"type": "done", "id": "c1"})
168        self.assertEqual(self.link.idle_for(), 0)
169
170    def test_job_stop_frame_dispatch(self):
171        connection, _ = self.attach()
172        frame = {"type": "job_stop", "id": "job-42"}
173        albedo_link.write_frame(
174            connection.send,
175            albedo_link.encode({"seq": 1, "ack": 0, "frame": frame}),
176        )
177        self.assertEqual(albedo_link.read_frame(connection.recv), {"ack": 1})
178        self.assertEqual(self.received, [frame])
179
180    def test_cells_serve_job_stop_unknown_job_answers_not_found(self):
181        import albedo_cells as kernel
182
183        sent = []
184        with patch.object(kernel, "send", sent.append):
185            kernel.LOOP.run_until_complete(kernel.serve_job_stop("missing-job"))
186        self.assertEqual(
187            sent,
188            [
189                {
190                    "type": "job_stop",
191                    "id": "missing-job",
192                    "ok": False,
193                    "error": "job not found",
194                }
195            ],
196        )
197
198
199if __name__ == "__main__":
200    unittest.main()