PyMkDB 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. pymkdb/__init__.py +6 -0
  2. pymkdb/cli.py +57 -0
  3. pymkdb-0.1.0.dist-info/METADATA +86 -0
  4. pymkdb-0.1.0.dist-info/RECORD +54 -0
  5. pymkdb-0.1.0.dist-info/WHEEL +5 -0
  6. pymkdb-0.1.0.dist-info/entry_points.txt +2 -0
  7. pymkdb-0.1.0.dist-info/top_level.txt +3 -0
  8. sdk/__init__.py +1 -0
  9. sdk/connection.py +225 -0
  10. sdk/delta.py +19 -0
  11. sdk/http_connection.py +180 -0
  12. sdk/mkdb_client.py +226 -0
  13. sdk/responses.py +154 -0
  14. src/__init__.py +1 -0
  15. src/config/db.py +227 -0
  16. src/config/server.py +52 -0
  17. src/db/__init__.py +207 -0
  18. src/db/cache/__init__.py +1 -0
  19. src/db/cache/ram_cache.py +144 -0
  20. src/db/cache/write_queue.py +156 -0
  21. src/db/maintenance/__init__.py +0 -0
  22. src/db/maintenance/compactor.py +118 -0
  23. src/db/maintenance/task_scheduler.py +73 -0
  24. src/db/objects/store.py +283 -0
  25. src/db/parity/__init__.py +0 -0
  26. src/db/parity/parity_manager.py +196 -0
  27. src/db/query/__init__.py +1 -0
  28. src/db/query/full_text_index.py +168 -0
  29. src/db/query/numeric_index.py +196 -0
  30. src/db/query/query_engine.py +308 -0
  31. src/db/query/tokenizer.py +48 -0
  32. src/db/query_workers/__init__.py +16 -0
  33. src/db/query_workers/dispatcher.py +339 -0
  34. src/db/query_workers/task.py +78 -0
  35. src/db/query_workers/worker.py +292 -0
  36. src/db/requesting/main.py +0 -0
  37. src/db/storage/__init__.py +1 -0
  38. src/db/storage/blob_store.py +47 -0
  39. src/db/storage/index_manager.py +92 -0
  40. src/db/storage/log_manager.py +119 -0
  41. src/db/storage/serializer.py +38 -0
  42. src/filing/__init__.py +31 -0
  43. src/objects/__init__.py +190 -0
  44. src/runtime/__init__.py +15 -0
  45. src/server/__init__.py +0 -0
  46. src/server/coms/actions.py +209 -0
  47. src/server/coms/http.py +46 -0
  48. src/server/coms/http_handlers.py +445 -0
  49. src/server/coms/metrics.py +231 -0
  50. src/server/coms/socket.py +461 -0
  51. src/server/coms/socket_protocol.py +54 -0
  52. src/server/control/api/actions.py +1001 -0
  53. src/server/control/server.py +404 -0
  54. src/server/event_log.py +58 -0
@@ -0,0 +1,73 @@
1
+ """
2
+ TaskScheduler — periodic daemon thread that checks segments and enqueues
3
+ compaction tasks via WriteQueue.
4
+ """
5
+
6
+ import logging
7
+ import threading
8
+ import time
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class TaskScheduler:
14
+ def __init__(self, store, interval: float = None):
15
+ """
16
+ Parameters
17
+ ----------
18
+ store : src.db.objects.store.store
19
+ interval : check interval in seconds (default from file_config or 60)
20
+ """
21
+ self._store = store
22
+ if interval is None:
23
+ interval = float(getattr(store.config.file_config, "compaction_interval", 60))
24
+ self.interval = interval
25
+ self._stop_event = threading.Event()
26
+ self._thread = threading.Thread(
27
+ target=self._loop, daemon=True, name=f"TaskScheduler-{store.config.name}"
28
+ )
29
+
30
+ def start(self) -> None:
31
+ self._thread.start()
32
+
33
+ def stop(self) -> None:
34
+ self._stop_event.set()
35
+
36
+ def trigger_now(self, task_type: str = "compact") -> None:
37
+ """Immediately enqueue a compaction task for all eligible segments."""
38
+ self._run_checks(task_type)
39
+
40
+ # ------------------------------------------------------------------
41
+ # Internal loop
42
+ # ------------------------------------------------------------------
43
+
44
+ def _loop(self) -> None:
45
+ while not self._stop_event.wait(timeout=self.interval):
46
+ self._run_checks("compact")
47
+
48
+ def _run_checks(self, task_type: str) -> None:
49
+ store = self._store
50
+ if store.log_manager is None or store._write_queue is None:
51
+ return
52
+ from src.db.maintenance.compactor import Compactor
53
+ compactor = Compactor(store)
54
+
55
+ segments = store.log_manager.list_segments()
56
+ # Skip the active segment
57
+ active = store.log_manager.active_segment
58
+ for seq_int in segments:
59
+ if seq_int == active:
60
+ continue
61
+ seq_str = f"{seq_int:03d}"
62
+ try:
63
+ if task_type == "compact" and compactor.needs_compaction(seq_str):
64
+ logger.info("TaskScheduler: enqueuing compact for segment %s", seq_str)
65
+ store._write_queue.enqueue({
66
+ "op": "compact",
67
+ "store": store.config.name,
68
+ "record_id": None,
69
+ "delta": {"segment": seq_int},
70
+ "ts": time.time(),
71
+ })
72
+ except Exception as exc:
73
+ logger.warning("TaskScheduler: error checking segment %s: %s", seq_str, exc)
@@ -0,0 +1,283 @@
1
+ import os
2
+
3
+ from src.db import mkdb
4
+ from src.config.db import store_config as _store_config
5
+ from src.db.query_workers import QueryDispatcher
6
+ from src.db.storage.log_manager import LogManager
7
+ from src.db.storage.index_manager import IndexManager
8
+ from src.db.storage import blob_store
9
+ from src.db.storage import serializer as _serializer
10
+ from src.db.cache.ram_cache import RamCache
11
+ from src.db.cache.write_queue import WriteQueue
12
+ from src.db.query.query_engine import QueryEngine, QuerySyntaxError
13
+ from src.db.maintenance.compactor import Compactor
14
+ from src.db.maintenance.task_scheduler import TaskScheduler
15
+ from src.db.parity.parity_manager import ParityManager
16
+
17
+
18
+ class store:
19
+ def __init__(self, db: mkdb, store_config: _store_config):
20
+ self.db = db
21
+ self.config = store_config
22
+ self._dispatcher: QueryDispatcher | None = None
23
+ self.log_manager: LogManager | None = None
24
+ self.index_manager: IndexManager | None = None
25
+ self._ram_cache: RamCache | None = None
26
+ self._write_queue: WriteQueue | None = None
27
+ self.query_engine: QueryEngine | None = None
28
+ self._compactor: Compactor | None = None
29
+ self._task_scheduler: TaskScheduler | None = None
30
+ self._parity_manager: ParityManager | None = None
31
+
32
+ @property
33
+ def store_path(self):
34
+ return os.path.join(self.db.file_path, "stores", self.config.name)
35
+
36
+ @property
37
+ def dispatcher(self) -> QueryDispatcher:
38
+ """Lazy-initialised query dispatcher for this store."""
39
+ if self._dispatcher is None:
40
+ self._dispatcher = QueryDispatcher(
41
+ store_name=self.config.name,
42
+ base_path=self.store_path,
43
+ config=self.config.query_worker_config,
44
+ )
45
+ self._dispatcher.start()
46
+ return self._dispatcher
47
+
48
+ def setup(self):
49
+ """Setup the store based on the configuration."""
50
+ os.makedirs(self.store_path, exist_ok=True)
51
+ seg_threshold = getattr(self.config.file_config, "segment_threshold",
52
+ self.config.file_config.file_size_split_trigger)
53
+ self.log_manager = LogManager(
54
+ store_path=self.store_path,
55
+ service=self.config.name,
56
+ segment_threshold=seg_threshold,
57
+ )
58
+ self.index_manager = IndexManager(
59
+ store_path=self.store_path,
60
+ service=self.config.name,
61
+ )
62
+ print(f"Store '{self.config.name}' initialized at {self.store_path}")
63
+ from src.server.event_log import emit as _emit
64
+ _emit("info", f"store:{self.config.name}", f"Store loaded from {self.store_path}")
65
+
66
+ # Parity manager — boot verification runs at startup
67
+ nsym = getattr(self.config.file_config, "parity_nsym", 10)
68
+ self._parity_manager = ParityManager(
69
+ store_path=self.store_path,
70
+ service=self.config.name,
71
+ nsym=nsym,
72
+ )
73
+ self._parity_manager.boot_verify_all(self.log_manager)
74
+
75
+ # RAM cache
76
+ self._ram_cache = RamCache(
77
+ max_size=self.config.ram_config.max_size,
78
+ ttl=self.config.ram_config.ttl,
79
+ clean_type=self.config.ram_config.clean_type,
80
+ )
81
+
82
+ # Write queue
83
+ debounce = getattr(self.config, "write_queue_config",
84
+ None)
85
+ debounce_window = debounce.debounce_window if debounce else 5.0
86
+ max_pending = debounce.max_pending if debounce else 10_000
87
+ self._write_queue = WriteQueue(
88
+ debounce_window=debounce_window,
89
+ max_pending=max_pending,
90
+ )
91
+
92
+ def _handle_write(task: dict) -> None:
93
+ """Flush a debounced write task to the log/index layer."""
94
+ rid = task["record_id"]
95
+ delta = task["delta"]
96
+ # Capture the active segment before write so we can detect rollover
97
+ seg_before = self.log_manager.active_segment if self.log_manager else None
98
+ # Call the low-level write (defined by WS-1); it writes to log + index
99
+ self._write_to_storage(rid, delta)
100
+ if self.query_engine is not None:
101
+ self.query_engine.on_write(rid, delta)
102
+ self.invalidate_cache(rid)
103
+ # On segment rollover: seal the old segment with parity + flush indexes
104
+ if self.log_manager and seg_before is not None and self.log_manager.active_segment != seg_before:
105
+ if self.query_engine is not None:
106
+ self.query_engine.save_all()
107
+ if self._parity_manager is not None:
108
+ try:
109
+ self._parity_manager.generate(seg_before)
110
+ except Exception:
111
+ pass # parity failure must never block writes
112
+
113
+ self._write_queue.register_handler("write", _handle_write)
114
+ self._write_queue.start()
115
+
116
+ # Query engine
117
+ self.query_engine = QueryEngine(self)
118
+ self.query_engine.build_indexes()
119
+
120
+ # Compaction
121
+ self._compactor = Compactor(self)
122
+
123
+ # Register compact handler on write queue
124
+ def _handle_compact(task: dict) -> None:
125
+ seq_int = task.get("delta", {}).get("segment")
126
+ if seq_int is not None:
127
+ self._compactor.compact_segment(int(seq_int))
128
+ if self._parity_manager is not None:
129
+ try:
130
+ self._parity_manager.generate(int(seq_int))
131
+ except ImportError:
132
+ pass # reedsolo not installed; silently skip
133
+
134
+ self._write_queue.register_handler("compact", _handle_compact)
135
+
136
+ # Task scheduler
137
+ self._task_scheduler = TaskScheduler(self)
138
+ self._task_scheduler.start()
139
+
140
+ def query(self, operation: str, params: dict, timeout: float | None = None):
141
+ """
142
+ Submit a query task to the worker pool and return the result.
143
+
144
+ Parameters
145
+ ----------
146
+ operation : str "read" | "query" | "count" | "exists" | "multi_read"
147
+ params : dict Operation-specific parameters.
148
+ timeout : float Optional per-call timeout override.
149
+ """
150
+ return self.dispatcher.submit(operation, params, timeout=timeout)
151
+
152
+ def invalidate_cache(self, record_id: str) -> None:
153
+ """
154
+ Broadcast a cache-invalidation for record_id to all query workers.
155
+ Called by the write queue after a record is flushed to disk.
156
+ """
157
+ if self._dispatcher is not None:
158
+ self._dispatcher.invalidate(record_id)
159
+
160
+ def stop(self) -> None:
161
+ """Gracefully stop the query worker pool for this store."""
162
+ if self._dispatcher is not None:
163
+ self._dispatcher.stop()
164
+ self._dispatcher = None
165
+
166
+ def generate_id(self) -> str:
167
+ """Generate a unique record ID using this store's entity_config.
168
+
169
+ Samples random characters from ``entity_config.token_chars`` at
170
+ ``entity_config.token_length``. Collision-checks against the live
171
+ index. If every attempt at the current length fails AND
172
+ ``entity_config.auto_expand`` is True, the token_length is incremented
173
+ by 1, the config is saved, and generation retries at the new length.
174
+ """
175
+ import random
176
+ ec = self.config.entity_config
177
+ chars = ec.token_chars or "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
178
+ max_attempts_per_length = 200
179
+
180
+ while True:
181
+ length = ec.token_length
182
+ for _ in range(max_attempts_per_length):
183
+ candidate = "".join(random.choices(chars, k=length))
184
+ # Accept immediately if the index says it doesn't exist
185
+ if self.index_manager is None or self.index_manager.get(candidate) is None:
186
+ return candidate
187
+
188
+ # Exhausted attempts — keyspace is likely saturated at this length
189
+ if ec.auto_expand:
190
+ ec.token_length += 1
191
+ self.db.config.save()
192
+ # Loop again with the expanded length
193
+ else:
194
+ raise RuntimeError(
195
+ f"Could not generate a unique ID for store '{self.config.name}' "
196
+ f"after {max_attempts_per_length} attempts at token_length={length}. "
197
+ "Enable auto_expand in the store's entity config, or increase token_length."
198
+ )
199
+
200
+ def write(self, record_id: str, flat_dict: dict) -> None:
201
+ """Write a record — updates cache immediately, flushes to disk via queue."""
202
+ if self._ram_cache is not None:
203
+ self._ram_cache.apply_delta(record_id, flat_dict)
204
+ if self._write_queue is not None:
205
+ self._write_queue.enqueue({
206
+ "op": "write",
207
+ "store": self.config.name,
208
+ "record_id": record_id,
209
+ "delta": flat_dict,
210
+ "ts": __import__("time").time(),
211
+ })
212
+ else:
213
+ # Fallback: direct write if queue not initialised
214
+ self._write_to_storage(record_id, flat_dict)
215
+
216
+ def _write_to_storage(self, record_id: str, flat_dict: dict) -> None:
217
+ """Write directly to log + index (bypasses cache and queue)."""
218
+ if self.log_manager is None or self.index_manager is None:
219
+ raise RuntimeError("Store is not set up. Call setup() first.")
220
+ blob_threshold = getattr(self.config.file_config, "blob_threshold", 5 * 1024 * 1024)
221
+ from src.db.storage import serializer as _serializer
222
+ from src.db.storage import blob_store
223
+ line_str = _serializer.serialize_record(record_id, flat_dict)
224
+ if len(line_str.encode("utf-8")) > blob_threshold:
225
+ seg, offset, size = blob_store.write_blob(self.store_path, record_id, line_str)
226
+ else:
227
+ seg, offset, size = self.log_manager.append(record_id, line_str)
228
+ self.index_manager.set(record_id, seg, offset, size)
229
+
230
+ def read(self, record_id: str) -> dict | None:
231
+ """Read a record — checks RAM cache first, falls back to disk."""
232
+ if self._ram_cache is not None:
233
+ cached = self._ram_cache.get(record_id)
234
+ if cached is not None:
235
+ return cached
236
+ if self.index_manager is None:
237
+ raise RuntimeError("Store is not set up. Call setup() first.")
238
+ entry = self.index_manager.get(record_id)
239
+ if entry is None:
240
+ return None
241
+ seg, offset, size = entry
242
+ if self.log_manager is None:
243
+ raise RuntimeError("Store is not set up. Call setup() first.")
244
+ from src.db.storage import serializer as _serializer
245
+ line_str = self.log_manager.read(seg, offset, size)
246
+ _, flat_dict = _serializer.deserialize_record(line_str)
247
+ if self._ram_cache is not None:
248
+ self._ram_cache.set(record_id, flat_dict)
249
+ return flat_dict
250
+
251
+ def delete(self, record_id: str) -> None:
252
+ """Soft-delete a record."""
253
+ if self._ram_cache is not None:
254
+ self._ram_cache.delete(record_id)
255
+ if self.index_manager is None:
256
+ raise RuntimeError("Store is not set up. Call setup() first.")
257
+ self.index_manager.delete(record_id)
258
+ # Update query indexes
259
+ if self.query_engine is not None:
260
+ # Read old values for index removal — if not in cache, skip (index will be stale until rebuild)
261
+ old = self._ram_cache.get(record_id) if self._ram_cache else None
262
+ if old:
263
+ self.query_engine.on_delete(record_id, old)
264
+
265
+ def teardown(self) -> None:
266
+ """Flush and close storage handles. Called during server shutdown."""
267
+ from src.server.event_log import emit as _emit
268
+ _emit("info", f"store:{self.config.name}", "Store shutting down — flushing writes")
269
+ if self._task_scheduler is not None:
270
+ self._task_scheduler.stop()
271
+ # Flush the write queue FIRST so all pending writes hit the log and
272
+ # update the query engine's in-memory indexes before we save them.
273
+ if self._write_queue is not None:
274
+ self._write_queue.flush_all()
275
+ self._write_queue.stop()
276
+ if self.query_engine is not None:
277
+ self.query_engine.save_all()
278
+ print(f"[{self.config.name}] Query indexes saved.")
279
+ if self.index_manager is not None and self.index_manager._dirty:
280
+ self.index_manager.save_full()
281
+ if self.log_manager is not None:
282
+ self.log_manager.close()
283
+ self.stop() # stops query dispatcher
File without changes
@@ -0,0 +1,196 @@
1
+ """
2
+ ParityManager — Reed-Solomon parity generation and verification for log segments.
3
+
4
+ Parity files: {store_path}/{service}_{NNN}.parity
5
+ These files contain the Reed-Solomon parity symbols for the corresponding .log segment.
6
+
7
+ External dependency: reedsolo >= 1.7 (pip install reedsolo)
8
+ Imports are deferred to operation methods so the module loads cleanly even
9
+ if reedsolo is not installed; a clear ImportError with install instructions
10
+ is raised only when a parity operation is invoked.
11
+ """
12
+
13
+ import logging
14
+ import os
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ _REEDSOLO_MISSING = (
19
+ "reedsolo is required for parity operations. "
20
+ "Install it with: pip install reedsolo"
21
+ )
22
+
23
+
24
+ class ParityManager:
25
+ def __init__(self, store_path: str, service: str, nsym: int = 10):
26
+ """
27
+ Parameters
28
+ ----------
29
+ store_path : absolute path to the store directory
30
+ service : store name (file prefix)
31
+ nsym : Reed-Solomon parity symbol count (default 10)
32
+ """
33
+ self.store_path = store_path
34
+ self.service = service
35
+ self.nsym = nsym
36
+
37
+ # ------------------------------------------------------------------
38
+ # Path helpers
39
+ # ------------------------------------------------------------------
40
+
41
+ def _log_path(self, seq_int: int) -> str:
42
+ return os.path.join(self.store_path, f"{self.service}_{seq_int:03d}.log")
43
+
44
+ def _parity_path(self, seq_int: int) -> str:
45
+ return os.path.join(self.store_path, f"{self.service}_{seq_int:03d}.parity")
46
+
47
+ # ------------------------------------------------------------------
48
+ # Public API
49
+ # ------------------------------------------------------------------
50
+
51
+ def generate(self, seq_int: int) -> None:
52
+ """
53
+ Read the .log segment, encode via Reed-Solomon, write parity to .parity file.
54
+ Overwrites any existing .parity file.
55
+
56
+ Parity file format: concatenated ECC symbols for each chunk
57
+ (nsym bytes per chunk of chunk_size = nsize-nsym data bytes).
58
+ This avoids the single-chunk limitation of GF(2^8) (max 255 bytes)
59
+ and keeps parity files small (~nsym/chunk_size ≈ 4% of log size).
60
+ """
61
+ try:
62
+ import reedsolo
63
+ except ImportError:
64
+ raise ImportError(_REEDSOLO_MISSING)
65
+
66
+ log_path = self._log_path(seq_int)
67
+ if not os.path.exists(log_path):
68
+ logger.warning("generate: log segment %03d not found, skipping", seq_int)
69
+ return
70
+
71
+ with open(log_path, "rb") as fh:
72
+ data = fh.read()
73
+
74
+ codec = reedsolo.RSCodec(self.nsym)
75
+ chunk_size = codec.nsize - codec.nsym # data bytes per chunk (default 245)
76
+ ecc_parts = []
77
+ for i in range(0, len(data), chunk_size):
78
+ chunk = data[i:i + chunk_size]
79
+ enc_chunk = bytearray(codec.encode(chunk))
80
+ ecc_parts.append(enc_chunk[len(chunk):]) # only the nsym ECC bytes
81
+ parity_bytes = b"".join(ecc_parts)
82
+
83
+ parity_path = self._parity_path(seq_int)
84
+ tmp_path = parity_path + ".tmp"
85
+ with open(tmp_path, "wb") as fh:
86
+ fh.write(parity_bytes)
87
+ os.replace(tmp_path, parity_path)
88
+ logger.debug("generate: parity written for segment %03d (%d bytes)", seq_int, len(parity_bytes))
89
+
90
+ def verify_and_repair(self, seq_int: int) -> bool:
91
+ """
92
+ Verify and optionally repair a log segment using its parity file.
93
+
94
+ Returns
95
+ -------
96
+ True — segment is clean or was repaired successfully
97
+ False — segment is corrupt beyond repair
98
+ """
99
+ try:
100
+ import reedsolo
101
+ except ImportError:
102
+ raise ImportError(_REEDSOLO_MISSING)
103
+
104
+ log_path = self._log_path(seq_int)
105
+ parity_path = self._parity_path(seq_int)
106
+
107
+ if not os.path.exists(log_path):
108
+ logger.warning("verify_and_repair: log segment %03d not found", seq_int)
109
+ return False
110
+ if not os.path.exists(parity_path):
111
+ logger.warning("verify_and_repair: parity file for segment %03d not found; generating now", seq_int)
112
+ self.generate(seq_int)
113
+ return True # freshly generated == clean by definition
114
+
115
+ with open(log_path, "rb") as fh:
116
+ log_bytes = fh.read()
117
+ with open(parity_path, "rb") as fh:
118
+ parity_bytes = fh.read()
119
+
120
+ codec = reedsolo.RSCodec(self.nsym)
121
+ chunk_size = codec.nsize - codec.nsym # data bytes per chunk (default 245)
122
+ repaired = bytearray()
123
+ ecc_offset = 0
124
+ try:
125
+ for i in range(0, len(log_bytes), chunk_size):
126
+ chunk = log_bytes[i:i + chunk_size]
127
+ ecc = parity_bytes[ecc_offset:ecc_offset + codec.nsym]
128
+ dec_tuple = codec.decode(bytes(chunk) + bytes(ecc))
129
+ repaired.extend(dec_tuple[0])
130
+ ecc_offset += codec.nsym
131
+ except reedsolo.ReedSolomonError as exc:
132
+ logger.error(
133
+ "verify_and_repair: segment %03d is corrupt beyond repair: %s",
134
+ seq_int, exc,
135
+ )
136
+ return False
137
+
138
+ decoded_bytes = bytes(repaired)
139
+ if decoded_bytes != log_bytes:
140
+ # Corruption detected but repairable
141
+ logger.warning(
142
+ "verify_and_repair: segment %03d had corruption — repaired and rewritten",
143
+ seq_int,
144
+ )
145
+ from src.server.event_log import emit as _emit
146
+ _emit("recovery", f"store:{self.service}",
147
+ f"Segment {self.service}_{seq_int:03d}.log had corruption — repaired via Reed-Solomon")
148
+ tmp_path = log_path + ".tmp"
149
+ with open(tmp_path, "wb") as fh:
150
+ fh.write(decoded_bytes)
151
+ os.replace(tmp_path, log_path)
152
+
153
+ return True
154
+
155
+ def boot_verify_all(self, log_manager) -> None:
156
+ """
157
+ Called from store.setup() after LogManager is initialised.
158
+ Iterates all sealed segments and calls verify_and_repair on each.
159
+ The active (highest-numbered) segment is excluded from verification
160
+ because it is still being appended to — its parity is always regenerated
161
+ fresh instead so subsequent rollover-based parity generation is correct.
162
+ """
163
+ from src.server.event_log import emit as _emit
164
+ all_segs = log_manager.list_segments()
165
+ active = log_manager.active_segment
166
+ for seq_int in all_segs:
167
+ if seq_int == active:
168
+ # Always regenerate parity for the active segment — it may have
169
+ # grown since the last time parity was written.
170
+ try:
171
+ self.generate(seq_int)
172
+ logger.debug("boot_verify_all: regenerated parity for active segment %03d", seq_int)
173
+ except ImportError:
174
+ logger.info(
175
+ "boot_verify_all: reedsolo not installed — parity skipped. "
176
+ "Install with: pip install reedsolo"
177
+ )
178
+ except Exception as exc:
179
+ logger.warning("boot_verify_all: could not generate parity for active segment %03d: %s", seq_int, exc)
180
+ continue
181
+ try:
182
+ ok = self.verify_and_repair(seq_int)
183
+ if not ok:
184
+ logger.error("boot_verify_all: segment %03d could not be repaired", seq_int)
185
+ _emit("error", f"store:{self.service}",
186
+ f"Segment {self.service}_{seq_int:03d}.log is corrupt beyond repair")
187
+ except ImportError:
188
+ logger.info(
189
+ "boot_verify_all: reedsolo not installed — parity verification skipped. "
190
+ "Install with: pip install reedsolo"
191
+ )
192
+ return # skip all remaining segments
193
+ except Exception as exc:
194
+ logger.warning("boot_verify_all: error on segment %03d: %s", seq_int, exc)
195
+ _emit("warning", f"store:{self.service}",
196
+ f"Parity check error on segment {self.service}_{seq_int:03d}.log: {exc}")
@@ -0,0 +1 @@
1
+