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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
/*
* Copyright (c) 2025-2026 Anton Kundenko <singaraiona@gmail.com>
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/* Transaction-log journaling — the `-l` / `-L` feature.
*
* Wire format: every entry is a complete IPC message (16-byte
* ray_ipc_header_t followed by serialized payload), so log frames
* share parser code with the live IPC path. Concatenation by `cat`
* is valid by construction.
*
* Open with ray_journal_open(base, mode):
* 1. If <base>.qdb exists, load it and bind every key into the global env.
* 2. If <base>.log exists, replay it (badtail is fatal, eval errors warn).
* 3. Open <base>.log for append.
*
* After open, the IPC dispatch hook (eval_payload in core/ipc.c) calls
* ray_journal_write_bytes() for every inbound sync message before
* evaluating it. Async messages and responses are not journaled,
* journaling only the .z.ps stream.
*
* Replay is single-threaded by construction (it runs from main, before
* the poll loop starts) so the module is intentionally not thread-safe;
* the IPC dispatch loop is also single-threaded for eval_payload, so the
* shared file handle does not need a mutex either.
*/
typedef enum ray_journal_mode_t;
typedef enum ray_jreplay_status_t;
/* Open the journal: load <base>.qdb, replay <base>.log, open log for append.
* Returns RAY_OK on success. Prints a one-line summary to stderr
* ("log: replayed N entries (M eval errors)"). Returns RAY_ERR_DOMAIN
* if the log replay hits a badtail; the caller should print a recovery
* hint and exit non-zero. */
ray_err_t ;
/* Phases 1+2 of recovery: load <base>.qdb then replay <base>.log. Does
* NOT open the journal for writing. Composed into ray_journal_open (the
* -l/-L startup recovery path); not invoked directly by any caller, and
* NOT used by the .log.open verb. */
ray_err_t ;
/* Open <base>.log for append in `mode` WITHOUT loading the snapshot or
* replaying. This is what the .log.open verb calls (resume appending
* without surprising re-execution of the whole log). */
ray_err_t ;
/* True iff a journal is currently open for append. */
bool ;
/* Append one entry to the active journal. No-op (returns RAY_OK) if
* no journal is open or if a replay is currently in progress (we do
* NOT recursively log replayed messages even if .log.write is called
* from a replayed entry). In RAY_JOURNAL_SYNC mode, fflush + fsync
* before returning. */
ray_err_t ;
/* Replay a log file, evaluating each entry in order. Sets *out_chunks
* to entries successfully replayed and *out_eval_errors to entries that
* deserialized cleanly but raised an error during ray_eval (those are
* skipped with a stderr warning, not fatal — framing was intact).
* *out_status is RAY_JREPLAY_OK on a clean tail or RAY_JREPLAY_BADTAIL
* if a truncated/corrupt frame was found. */
ray_err_t ;
/* Validate (parse but don't eval) frames without evaluating.
* *out_chunks counts valid entries; *out_valid_bytes is the byte
* offset of the first bad header (== file size on a clean log). */
ray_err_t ;
/* Close the active log, rename it to <base>.<UTC-ISO8601>.log, open a
* fresh empty <base>.log for append. Errors if no journal is open. */
ray_err_t ;
/* Serialize every user (non-reserved) global env binding into a dict and
* write it as a single entry to <base>.qdb.tmp, then atomic-rename to
* <base>.qdb, then call ray_journal_roll. After this, the .log file
* is fresh and a future restart loads .qdb instead of replaying the
* old (now archived) log. */
ray_err_t ;
/* Force fflush + fsync on the active journal. No-op (RAY_OK) when no
* journal is open or when in RAY_JOURNAL_SYNC mode (where every write
* already syncs). */
ray_err_t ;
/* Close the active journal. No-op if none is open. */
ray_err_t ;
/* Close the active journal (if open) and unlink EVERY on-disk file of the
* current journal base: the active <base>.log, every rolled
* <base>.<stamp>.log archive, the <base>.qdb snapshot, and a stray
* <base>.qdb.tmp. Operates on the same journal .log.open/.write/.close
* target — the base survives .log.close, so this works after a close.
* Resets module state to OFF so a later .log.open starts clean.
* Best-effort: a missing file is not an error; returns RAY_ERR_IO if an
* existing file cannot be removed, RAY_ERR_DOMAIN if no journal base is
* known (none ever opened this process). */
ray_err_t ;
/* RAY_JOURNAL_H */