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
/*
* 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 — q/kdb's `-l` / `-L` ported to Rayforce.
*
* 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,
* matching q's policy of 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 ;
/* 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) — q's `-11!(-2; file)` analogue.
* *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 ;
/* RAY_JOURNAL_H */