grepmax 0.26.64 → 0.26.65

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.
@@ -0,0 +1,200 @@
1
+ """Explicit, exclusively leased Lance 12 prune-only recovery candidate.
2
+
3
+ Never compacts, writes rows, builds indexes or starts a server. This helper is
4
+ not an automatic maintenance path. Native production resource validation is
5
+ required before enabling it on a pressured host.
6
+ """
7
+
8
+ import argparse
9
+ import importlib.metadata
10
+ import hashlib
11
+ import json
12
+ import os
13
+ from pathlib import Path
14
+ import sys
15
+ import time
16
+
17
+ MAX_FILES = 100_000
18
+ MAX_VERSIONS = 100_000
19
+ MAX_PROTECTED_VERSIONS = 256
20
+ CACHE_BYTES = 8 * 1024 * 1024
21
+
22
+
23
+ def apply_resource_limits():
24
+ # Must run before importing native libraries. Cache limits are not total
25
+ # memory limits. RLIMIT_RSS is not enforceable on macOS; do not claim it is.
26
+ for name in ("RAYON_NUM_THREADS", "TOKIO_WORKER_THREADS", "OMP_NUM_THREADS",
27
+ "OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS", "NUMEXPR_NUM_THREADS"):
28
+ os.environ[name] = "1"
29
+ import resource
30
+ resource.setrlimit(resource.RLIMIT_CPU, (90, 91))
31
+ resource.setrlimit(resource.RLIMIT_NOFILE, (256, 256))
32
+ if sys.platform.startswith("linux"):
33
+ resource.setrlimit(resource.RLIMIT_AS, (2 * 1024**3, 2 * 1024**3))
34
+
35
+
36
+ def verify_lease(root, owner_file, nonce):
37
+ expected = Path(str(root.parent) + ".lease") / "exclusive-intent" / "owner.json"
38
+ if Path(owner_file).resolve(strict=True) != expected.resolve(strict=True):
39
+ raise ValueError("Exclusive lease belongs to a different store")
40
+ if expected.is_symlink() or expected.stat().st_size > 16 * 1024:
41
+ raise ValueError("Exclusive lease owner is unverified or oversized")
42
+ owner = json.loads(expected.read_text())
43
+ helper = owner.get("activeHelper")
44
+ expected_pid = os.getpid() if helper and helper.get("pid") == os.getpid() else os.getppid()
45
+ if not nonce or owner.get("nonce") != nonce or owner.get("pid") != expected_pid:
46
+ raise ValueError("Exclusive lease ownership could not be verified")
47
+ readers = expected.parent.parent / "readers"
48
+ if not readers.is_dir() or any(readers.glob("*.json")):
49
+ raise ValueError("All shared store owners must close before pruning")
50
+ return owner
51
+
52
+
53
+ def measure(root):
54
+ total = allocated = count = 0
55
+ pending = [root]
56
+ while pending:
57
+ directory = pending.pop()
58
+ with os.scandir(directory) as entries:
59
+ for entry in entries:
60
+ count += 1
61
+ if count > MAX_FILES:
62
+ raise ValueError("Recovery metadata scan exceeded bounded file count")
63
+ if entry.is_symlink():
64
+ raise ValueError("Recovery refuses symlinks inside the table")
65
+ if entry.is_dir(follow_symlinks=False):
66
+ pending.append(Path(entry.path))
67
+ elif entry.is_file(follow_symlinks=False):
68
+ stat = entry.stat(follow_symlinks=False)
69
+ total += stat.st_size
70
+ allocated += stat.st_blocks * 512
71
+ else:
72
+ raise ValueError("Recovery refuses nonregular table entries")
73
+ space = os.statvfs(root)
74
+ return total, allocated, space.f_bavail * space.f_frsize
75
+
76
+
77
+ def tag_versions(dataset):
78
+ return {name: dataset.tags.get_version(name) for name in dataset.tags.list()}
79
+
80
+
81
+ def open_dataset(lance, root):
82
+ return lance.dataset(
83
+ str(root), index_cache_size_bytes=CACHE_BYTES,
84
+ metadata_cache_size_bytes=CACHE_BYTES,
85
+ read_params={"cache_repetition_index": False},
86
+ )
87
+
88
+
89
+ def snapshot(dataset):
90
+ return (
91
+ [fragment.metadata.to_json() for fragment in dataset.get_fragments()],
92
+ dataset.list_indices(),
93
+ dataset.schema.serialize().to_pybytes(),
94
+ tag_versions(dataset),
95
+ )
96
+
97
+
98
+ def protected_digest(dataset):
99
+ fragments, indices, schema, tags = snapshot(dataset)
100
+ value = [fragments, indices, schema.hex(), tags]
101
+ encoded = json.dumps(value, sort_keys=True, default=lambda item: sorted(item) if isinstance(item, set) else str(item))
102
+ return hashlib.sha256(encoded.encode()).hexdigest()
103
+
104
+
105
+ def open_version(lance, root, version):
106
+ return lance.dataset(str(root), version=version, index_cache_size_bytes=CACHE_BYTES,
107
+ metadata_cache_size_bytes=CACHE_BYTES)
108
+
109
+
110
+ def prune(store, expected_version, cutoff_ms, owner_file, nonce, admission=None):
111
+ root = Path(store).resolve(strict=True)
112
+ if not root.is_dir() or root.suffix != ".lance":
113
+ raise ValueError("Cleanup requires a local Lance table directory")
114
+ owner = verify_lease(root, owner_file, nonce)
115
+ if not isinstance(expected_version, int) or expected_version < 1:
116
+ raise ValueError("Cleanup requires a positive current version")
117
+ if cutoff_ms > time.time_ns() // 1_000_000:
118
+ raise ValueError("Cleanup cutoff must not be in the future")
119
+ if importlib.metadata.version("pylance") != "12.0.0":
120
+ raise ValueError("Cleanup requires pylance 12.0.0")
121
+ before_bytes, before_allocated, before_free = measure(root)
122
+ import lance
123
+ dataset = open_dataset(lance, root)
124
+ if dataset.version != expected_version or dataset.latest_version != expected_version:
125
+ raise ValueError("Table changed before cleanup")
126
+ current = snapshot(dataset)
127
+ all_versions = dataset.versions()
128
+ if len(all_versions) > MAX_VERSIONS:
129
+ raise ValueError("Recovery exceeded bounded version count")
130
+ protected = {expected_version, *current[3].values()}
131
+ versions = []
132
+ for version in all_versions:
133
+ if version["timestamp"].timestamp() * 1000 >= cutoff_ms:
134
+ protected.add(version["version"])
135
+ if version["version"] not in protected and version["timestamp"].timestamp() * 1000 < cutoff_ms:
136
+ versions.append(version["version"])
137
+ if dataset.latest_version != expected_version or verify_lease(root, owner_file, nonce) != owner:
138
+ raise ValueError("Table or exclusive ownership changed during cleanup admission")
139
+ if len(protected) > MAX_PROTECTED_VERSIONS:
140
+ raise ValueError("Protected metadata exceeds bounded recovery version count")
141
+ protected_state = {version: protected_digest(open_version(lance, root, version)) for version in protected}
142
+ if admission is not None:
143
+ admission()
144
+ if dataset.latest_version != expected_version or verify_lease(root, owner_file, nonce) != owner:
145
+ raise ValueError("Table or exclusive ownership changed at deletion boundary")
146
+ # Explicit versions preserve current/tagged/post-cutoff commits. Unverified
147
+ # files stay protected. The native operation cannot rewrite data/index files.
148
+ stats = dataset.cleanup_old_versions(
149
+ versions=versions, delete_unverified=False,
150
+ error_if_tagged_old_versions=True, delete_rate_limit=32,
151
+ ) if versions else None
152
+ fresh = open_dataset(lance, root)
153
+ remaining = {version["version"] for version in fresh.versions()}
154
+ if (fresh.version != expected_version or fresh.latest_version != expected_version
155
+ or snapshot(fresh) != current or not protected.issubset(remaining)
156
+ or set(versions).intersection(remaining)
157
+ or verify_lease(root, owner_file, nonce) != owner):
158
+ raise ValueError("Current/protected state or exclusive ownership changed; no retry")
159
+ if any(protected_digest(open_version(lance, root, version)) != digest
160
+ for version, digest in protected_state.items()):
161
+ raise ValueError("Protected state changed; no retry")
162
+ after_bytes, after_allocated, after_free = measure(root)
163
+ return {
164
+ "engine": "12.0.0", "version": expected_version,
165
+ "fragments": len(current[0]),
166
+ "bytesRemoved": stats.bytes_removed if stats else 0,
167
+ "versionsRemoved": stats.old_versions if stats else 0,
168
+ "rewritten": False,
169
+ "fileBytesBefore": before_bytes, "fileBytesAfter": after_bytes,
170
+ "allocatedBytesBefore": before_allocated, "allocatedBytesAfter": after_allocated,
171
+ "freeBytesBefore": before_free, "freeBytesAfter": after_free,
172
+ }
173
+
174
+
175
+ def main():
176
+ parser = argparse.ArgumentParser(description=__doc__)
177
+ parser.add_argument("--store", required=True)
178
+ parser.add_argument("--version", required=True, type=int)
179
+ parser.add_argument("--cutoff-ms", required=True, type=int)
180
+ parser.add_argument("--lease-owner", required=True)
181
+ parser.add_argument("--lease-nonce", required=True)
182
+ parser.add_argument("--require-admission", action="store_true")
183
+ args = parser.parse_args()
184
+ if not args.require_admission:
185
+ parser.error("Prune helper requires guarded gmax recovery admission")
186
+ apply_resource_limits()
187
+ if args.require_admission:
188
+ print(json.dumps({"admission": "launch"}), flush=True)
189
+ if sys.stdin.readline(256).strip() != args.lease_nonce:
190
+ raise ValueError("Launch admission unavailable; no cleanup")
191
+ def admission():
192
+ print(json.dumps({"admission": "ready"}), flush=True)
193
+ if sys.stdin.readline(256).strip() != args.lease_nonce:
194
+ raise ValueError("Deletion admission unavailable; no cleanup")
195
+ print(json.dumps(prune(args.store, args.version, args.cutoff_ms, args.lease_owner,
196
+ args.lease_nonce, admission if args.require_admission else None)))
197
+
198
+
199
+ if __name__ == "__main__":
200
+ main()
@@ -0,0 +1,9 @@
1
+ [project]
2
+ name = "gmax-lance-maintenance"
3
+ version = "0.1.0"
4
+ description = "Prune-only maintenance for grepmax's pinned Lance 12 store"
5
+ requires-python = ">=3.10"
6
+ dependencies = ["pylance==12.0.0"]
7
+
8
+ [tool.uv]
9
+ package = false