• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

knowledgepixels / nanopub-query / 30458093186

29 Jul 2026 01:52PM UTC coverage: 58.203% (-1.4%) from 59.566%
30458093186

push

github

web-flow
Merge pull request #144 from knowledgepixels/feat/retro-sweep

feat(reconciler): retro pass for verified-then-lost shards + healthcheck restart softening

620 of 1202 branches covered (51.58%)

Branch coverage included in aggregate %.

1796 of 2949 relevant lines covered (60.9%)

9.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

14.57
src/main/java/com/knowledgepixels/query/ShardReconciler.java
1
package com.knowledgepixels.query;
2

3
import org.eclipse.rdf4j.common.transaction.IsolationLevels;
4
import org.eclipse.rdf4j.model.IRI;
5
import org.eclipse.rdf4j.model.Literal;
6
import org.eclipse.rdf4j.model.Statement;
7
import org.eclipse.rdf4j.model.Value;
8
import org.eclipse.rdf4j.model.ValueFactory;
9
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
10
import org.eclipse.rdf4j.query.BindingSet;
11
import org.eclipse.rdf4j.query.QueryLanguage;
12
import org.eclipse.rdf4j.query.TupleQuery;
13
import org.eclipse.rdf4j.query.TupleQueryResult;
14
import org.eclipse.rdf4j.repository.RepositoryConnection;
15
import org.eclipse.rdf4j.repository.RepositoryResult;
16
import org.nanopub.MalformedNanopubException;
17
import org.nanopub.Nanopub;
18
import org.nanopub.NanopubImpl;
19
import org.nanopub.vocabulary.NPA;
20
import org.nanopub.vocabulary.NPX;
21
import org.slf4j.Logger;
22
import org.slf4j.LoggerFactory;
23

24
import java.util.*;
25

26
/**
27
 * Periodic consistency sweep over the per-nanopub repo fan-out (issue #139).
28
 *
29
 * <p>{@link NanopubLoader#executeLoading} treats a per-shard commit acknowledgement
30
 * as truth: once every shard task has reported success, the {@code meta} write is
31
 * committed and the load counter moves on. Issue #139 (and the earlier meta-behind-full
32
 * incident) showed that the backend can acknowledge a shard write that never becomes
33
 * durable — leaving a nanopub silently missing from one shard forever, while
34
 * {@code full}/{@code meta} look complete. This class is the safety net: instead of
35
 * trusting acks, it periodically compares durable end-state across repos and re-loads
36
 * what is missing.
37
 *
38
 * <p>Each {@link #tick()} processes a bounded window of recently loaded nanopubs:
39
 * <ol>
40
 *   <li>Enumerate nanopubs from the <em>driver</em> repo ({@code full}, or {@code meta}
41
 *       when the full repo is disabled) with a load number above the persisted
42
 *       checkpoint. In-flight loads are never flagged: the {@code meta} stamp is the
43
 *       loader's completion marker ({@link NanopubLoader#executeLoading} commits it
44
 *       only after every other shard task succeeded), so a nanopub present in
45
 *       {@code meta} is verified immediately regardless of age, while a nanopub not
46
 *       yet in {@code meta} halts the sweep until it either completes or exceeds the
47
 *       {@link #GRACE_MS} patience (at which point the stalled/lost {@code meta}
48
 *       write is itself the incident to repair). With {@code meta} as the driver,
49
 *       driver membership already implies completion.</li>
50
 *   <li>Derive each nanopub's expected shard repos from its admin metadata
51
 *       ({@code npa:hasValidSignatureForPublicKeyHash}, {@code npx:hasNanopubType})
52
 *       using the same type filters as the loader.</li>
53
 *   <li>Check {@code npa:hasLoadNumber} membership per shard in batched queries.</li>
54
 *   <li>For any nanopub with a missing shard: reconstruct the nanopub from the driver
55
 *       repo's four content graphs and re-run {@link NanopubLoader#load(Nanopub, long)},
56
 *       which is idempotent per repo and fills exactly the missing shards (including
57
 *       recomputed text filter literals and spaces extractions).</li>
58
 *   <li>Advance the checkpoint only past nanopubs that verified clean or were
59
 *       repaired; on a repair failure the tick stops so the next tick retries.</li>
60
 * </ol>
61
 *
62
 * <p>The checkpoint is initialised to the driver repo's current maximum load number on
63
 * the first tick (or when the driver repo changes), so only nanopubs loaded from then
64
 * on are swept. Historical gaps can be swept by manually lowering the
65
 * {@code npa:hasReconciliationCheckpoint} value in the admin repo.
66
 *
67
 * <p>Known limitations: nanopubs missing from the driver repo itself are not
68
 * detectable here (the registry resync path remains the authority for that), and the
69
 * {@code last30d} repo is not checked (its content expires anyway). The {@code spaces}
70
 * shard is only expected via the {@link SpacesExtractor#isSpaceRelevant} trigger-type
71
 * proxy; a trigger-typed nanopub whose extraction turns out empty is logged and
72
 * treated as consistent after re-verification.
73
 */
74
public class ShardReconciler {
75

76
    private static final Logger logger = LoggerFactory.getLogger(ShardReconciler.class);
9✔
77

78
    private static final ValueFactory vf = SimpleValueFactory.getInstance();
6✔
79

80
    static final IRI HAS_RECONCILIATION_CHECKPOINT = vf.createIRI(NPA.NAMESPACE + "hasReconciliationCheckpoint");
12✔
81
    // @ADMIN-TRIPLE-TABLE@ REPO, npa:hasReconciliationCheckpoint, LOAD_NUMBER, npa:graph, admin, driver-repo load number up to which shard consistency has been verified
82
    static final IRI HAS_RECONCILIATION_DRIVER = vf.createIRI(NPA.NAMESPACE + "hasReconciliationDriver");
12✔
83
    // @ADMIN-TRIPLE-TABLE@ REPO, npa:hasReconciliationDriver, REPO_NAME, npa:graph, admin, repo the reconciliation checkpoint refers to
84

85
    /**
86
     * Patience for nanopubs that are in the driver repo but not yet stamped in
87
     * {@code meta}: their fan-out is normally still in flight (the meta task is
88
     * deferred behind the other shards, and per-shard retry chains can run for
89
     * minutes), so the sweep halts in front of them instead of racing the loader
90
     * with spurious repairs. Once a nanopub's driver-repo load timestamp is older
91
     * than this, a missing {@code meta} stamp is no longer plausible in-flight
92
     * state and is treated as a lost shard write to repair. Nanopubs already in
93
     * {@code meta} are verified immediately — completion is proven, not timed.
94
     */
95
    static final long GRACE_MS = 30L * 60 * 1000;
96

97
    /**
98
     * Upper bound on nanopubs examined per tick, so a large backlog (e.g. after
99
     * downtime) drains incrementally instead of producing one huge query.
100
     */
101
    static final int MAX_NPS_PER_TICK = 1000;
102

103
    /**
104
     * How far below the checkpoint the retro pass re-verifies. The forward sweep
105
     * proves shard membership once and moves on — but the 2026-07-29 incident
106
     * (issue #142) showed the backend can revoke a write <em>after</em> it was
107
     * acknowledged and readable: a shard verified present at sweep time vanished
108
     * when the store's wedged state was discarded by a later restart. The retro
109
     * pass therefore re-verifies every nanopub whose driver-repo load timestamp
110
     * is within this window, even though their load numbers are at or below the
111
     * checkpoint. Losses older than this window can still be caught by manually
112
     * lowering the checkpoint ({@code scripts/set-reconciliation-checkpoint.sh}).
113
     */
114
    static final long RETRO_WINDOW_MS = 24L * 60 * 60 * 1000;
115

116
    /**
117
     * Cumulative count of nanopubs whose shard fan-out was verified (clean or
118
     * repaired) since process start. Read by {@link MetricsCollector}.
119
     */
120
    static volatile long checkedNanopubCount = 0;
6✔
121

122
    /**
123
     * Cumulative count of shard repos found missing and successfully re-loaded
124
     * since process start. Read by {@link MetricsCollector}. Any non-zero value
125
     * is evidence of the backend acknowledging a non-durable write.
126
     */
127
    static volatile long repairedShardCount = 0;
6✔
128

129
    /**
130
     * Cumulative count of shard losses detected by the retro pass: shards that a
131
     * previous sweep verified present and that have since vanished. Read by
132
     * {@link MetricsCollector}. This is the direct "backend revoked durable
133
     * state" incident counter (issue #142) — even sharper evidence than
134
     * {@link #repairedShardCount}, which also counts never-landed writes.
135
     */
136
    static volatile long relostShardCount = 0;
9✔
137

138
    private ShardReconciler() {
139
    }
140

141
    /**
142
     * Runs one bounded reconciliation pass: the forward sweep (nanopubs above
143
     * the checkpoint) followed by the retro pass (re-verification of the
144
     * trailing {@link #RETRO_WINDOW_MS} at or below the checkpoint). Never
145
     * throws: any failure is logged and retried on the next tick (the
146
     * checkpoint only advances past verified nanopubs; the retro pass is
147
     * stateless). No-op unless the reconciliation feature is enabled and the
148
     * service is READY.
149
     */
150
    public static void tick() {
151
        if (!FeatureFlags.reconciliationEnabled()) {
×
152
            return;
×
153
        }
154
        if (StatusController.get().getState().state != StatusController.State.READY) {
×
155
            return;
×
156
        }
157
        try {
158
            runTick();
×
159
        } catch (Exception ex) {
×
160
            logger.warn("Shard reconciliation tick failed; will retry on next tick: {}", ex.getMessage(), ex);
×
161
        }
×
162
        try {
163
            runRetroTick();
×
164
        } catch (Exception ex) {
×
165
            logger.warn("Shard retro-verification tick failed; will retry on next tick: {}", ex.getMessage(), ex);
×
166
        }
×
167
    }
×
168

169
    private static void runTick() {
170
        String driverRepo = FeatureFlags.fullRepoEnabled() ? "full" : "meta";
×
171

172
        Long checkpoint = readCheckpoint(driverRepo);
×
173
        if (checkpoint == null) {
×
174
            // First run (or driver repo changed): establish the baseline at the
175
            // driver's current max load number. Only nanopubs loaded after this
176
            // point are swept.
177
            long max = fetchMaxLoadNumber(driverRepo);
×
178
            persistCheckpoint(driverRepo, max);
×
179
            logger.info("Shard reconciliation checkpoint initialized at load number {} of repo '{}'", max, driverRepo);
×
180
            return;
×
181
        }
182

183
        List<WindowEntry> window = fetchWindow(driverRepo, checkpoint);
×
184
        if (window.isEmpty()) {
×
185
            return;
×
186
        }
187
        Map<IRI, NanopubShardInfo> shardInfos = fetchShardInfos(driverRepo, window);
×
188

189
        // Batched membership lookup: one query per distinct shard repo, covering
190
        // all window nanopubs that expect it.
191
        Map<String, List<IRI>> npsByShard = new LinkedHashMap<>();
×
192
        for (WindowEntry e : window) {
×
193
            NanopubShardInfo info = shardInfos.get(e.npId());
×
194
            if (info == null) {
×
195
                continue;
×
196
            }
197
            for (String repo : expectedShardRepos(e.npId(), info.pubkeyHash(), info.types(), driverRepo)) {
×
198
                npsByShard.computeIfAbsent(repo, k -> new ArrayList<>()).add(e.npId());
×
199
            }
×
200
        }
×
201
        Map<String, Set<IRI>> presentByShard = new HashMap<>();
×
202
        for (Map.Entry<String, List<IRI>> e : npsByShard.entrySet()) {
×
203
            presentByShard.put(e.getKey(), fetchPresentNanopubs(e.getKey(), e.getValue()));
×
204
        }
×
205

206
        long newCheckpoint = checkpoint;
×
207
        long checked = 0;
×
208
        long repaired = 0;
×
209
        try {
210
            for (WindowEntry e : window) {
×
211
                // Completion gate: meta is written last, so a nanopub already in meta
212
                // has finished its fan-out and can be verified immediately regardless
213
                // of age. A nanopub not yet in meta is normally an in-flight load —
214
                // halt the sweep in front of it (checkpoint stays put, next tick
215
                // re-examines) until it either completes or exceeds the patience
216
                // window, after which the missing meta stamp is itself the incident
217
                // and falls through to the regular repair path.
218
                boolean completed = "meta".equals(driverRepo)
×
219
                        || presentByShard.getOrDefault("meta", Set.of()).contains(e.npId());
×
220
                if (!completed && System.currentTimeMillis() - e.loadedAtMs() < GRACE_MS) {
×
221
                    logger.debug("Halting shard sweep at <{}> (load number {}): not yet in meta, likely in flight", e.npId(), e.loadNumber());
×
222
                    return;
×
223
                }
224
                NanopubShardInfo info = shardInfos.get(e.npId());
×
225
                if (info == null) {
×
226
                    // No pubkey-hash admin triple in the driver repo; nothing we can
227
                    // derive shards from. Loud, but don't stall the sweep on it.
228
                    logger.warn("Skipping shard reconciliation for <{}>: no npa:hasValidSignatureForPublicKeyHash found in repo '{}'", e.npId(), driverRepo);
×
229
                    newCheckpoint = e.loadNumber();
×
230
                    continue;
×
231
                }
232
                List<String> missing = new ArrayList<>();
×
233
                for (String repo : expectedShardRepos(e.npId(), info.pubkeyHash(), info.types(), driverRepo)) {
×
234
                    if (!presentByShard.getOrDefault(repo, Set.of()).contains(e.npId())) {
×
235
                        missing.add(repo);
×
236
                    }
237
                }
×
238
                if (!missing.isEmpty()) {
×
239
                    if (!repairNanopub(driverRepo, e.npId(), missing)) {
×
240
                        // Repair failed: stop here so the checkpoint stays before this
241
                        // nanopub and the next tick retries.
242
                        return;
×
243
                    }
244
                    repaired += missing.size();
×
245
                }
246
                newCheckpoint = e.loadNumber();
×
247
                checked++;
×
248
            }
×
249
        } finally {
250
            checkedNanopubCount += checked;
×
251
            repairedShardCount += repaired;
×
252
            if (newCheckpoint > checkpoint) {
×
253
                persistCheckpoint(driverRepo, newCheckpoint);
×
254
            }
255
        }
256
        if (repaired > 0) {
×
257
            logger.warn("Shard reconciliation repaired {} missing shard(s) across {} nanopub(s); the backend acknowledged non-durable writes (see issue #139)", repaired, checked);
×
258
        } else {
259
            logger.debug("Shard reconciliation verified {} nanopub(s) up to load number {} of repo '{}'", checked, newCheckpoint, driverRepo);
×
260
        }
261
    }
×
262

263
    /**
264
     * Retro pass: re-verifies nanopubs the forward sweep already passed —
265
     * everything with a driver-repo load timestamp inside {@link #RETRO_WINDOW_MS}
266
     * and a load number at or below the checkpoint. A shard found missing here
267
     * was present when the forward sweep verified it, i.e. the backend revoked
268
     * previously readable state (issue #142) — counted separately in
269
     * {@link #relostShardCount} and logged loudly with the nanopub IRI so the
270
     * incident window is pinned for server-side log capture. Unlike the forward
271
     * sweep, a failed repair does not halt anything: the pass is stateless and
272
     * re-derives its window from time on every tick, so it retries automatically
273
     * until the loss ages out of the window.
274
     */
275
    private static void runRetroTick() {
276
        String driverRepo = FeatureFlags.fullRepoEnabled() ? "full" : "meta";
×
277
        Long checkpoint = readCheckpoint(driverRepo);
×
278
        if (checkpoint == null) {
×
279
            return;
×
280
        }
281
        List<WindowEntry> window = fetchRetroWindow(driverRepo, checkpoint);
×
282
        if (window.isEmpty()) {
×
283
            return;
×
284
        }
285
        Map<IRI, NanopubShardInfo> shardInfos = fetchShardInfos(driverRepo, window);
×
286

287
        Map<String, List<IRI>> npsByShard = new LinkedHashMap<>();
×
288
        for (WindowEntry e : window) {
×
289
            NanopubShardInfo info = shardInfos.get(e.npId());
×
290
            if (info == null) {
×
291
                continue;
×
292
            }
293
            for (String repo : expectedShardRepos(e.npId(), info.pubkeyHash(), info.types(), driverRepo)) {
×
294
                npsByShard.computeIfAbsent(repo, k -> new ArrayList<>()).add(e.npId());
×
295
            }
×
296
        }
×
297
        Map<String, Set<IRI>> presentByShard = new HashMap<>();
×
298
        for (Map.Entry<String, List<IRI>> e : npsByShard.entrySet()) {
×
299
            presentByShard.put(e.getKey(), fetchPresentNanopubs(e.getKey(), e.getValue()));
×
300
        }
×
301

302
        long relost = 0;
×
303
        long repaired = 0;
×
304
        for (WindowEntry e : window) {
×
305
            NanopubShardInfo info = shardInfos.get(e.npId());
×
306
            if (info == null) {
×
307
                continue;
×
308
            }
309
            List<String> missing = new ArrayList<>();
×
310
            for (String repo : expectedShardRepos(e.npId(), info.pubkeyHash(), info.types(), driverRepo)) {
×
311
                if (!presentByShard.getOrDefault(repo, Set.of()).contains(e.npId())) {
×
312
                    missing.add(repo);
×
313
                }
314
            }
×
315
            if (missing.isEmpty()) {
×
316
                continue;
×
317
            }
318
            logger.warn("Previously verified nanopub <{}> (load number {}) has LOST shard(s) {} — the backend revoked readable state (issue #142); capture the RDF4J server logs around now before restarting", e.npId(), e.loadNumber(), missing);
×
319
            relost += missing.size();
×
320
            if (repairNanopub(driverRepo, e.npId(), missing)) {
×
321
                repaired += missing.size();
×
322
            }
323
        }
×
324
        relostShardCount += relost;
×
325
        repairedShardCount += repaired;
×
326
        if (relost > 0) {
×
327
            logger.warn("Shard retro-verification found {} lost shard(s) across the last {} h window; {} re-repaired", relost, RETRO_WINDOW_MS / 3_600_000, repaired);
×
328
        } else {
329
            logger.debug("Shard retro-verification: {} nanopub(s) in the trailing window still consistent", window.size());
×
330
        }
331
    }
×
332

333
    /**
334
     * Fetches the retro window: nanopubs at or below the checkpoint whose driver
335
     * load timestamp falls inside {@link #RETRO_WINDOW_MS}. Ordered by
336
     * descending load number so that, when the window exceeds
337
     * {@link #MAX_NPS_PER_TICK} (e.g. during a resync), the newest — most
338
     * loss-prone — nanopubs are always covered.
339
     */
340
    private static List<WindowEntry> fetchRetroWindow(String driverRepo, long checkpoint) {
341
        List<WindowEntry> window = new ArrayList<>();
×
342
        try (RepositoryConnection conn = TripleStore.get().getRepoConnection(driverRepo)) {
×
343
            TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL,
×
344
                    "SELECT ?np ?ln ?ts WHERE { graph <" + NPA.GRAPH + "> { "
345
                    + "?np <" + NPA.HAS_LOAD_NUMBER + "> ?ln ; <" + NPA.HAS_LOAD_TIMESTAMP + "> ?ts . "
346
                    + "FILTER (?ln <= " + checkpoint + ") FILTER (?ts >= ?cutoff) "
347
                    + "} } ORDER BY DESC(?ln) LIMIT " + MAX_NPS_PER_TICK);
348
            q.setBinding("cutoff", vf.createLiteral(new Date(System.currentTimeMillis() - RETRO_WINDOW_MS)));
×
349
            try (TupleQueryResult r = q.evaluate()) {
×
350
                while (r.hasNext()) {
×
351
                    BindingSet b = r.next();
×
352
                    if (b.getValue("np") instanceof IRI npId && b.getValue("ts") instanceof Literal tsLit) {
×
353
                        window.add(new WindowEntry(npId, Long.parseLong(b.getValue("ln").stringValue()),
×
354
                                tsLit.calendarValue().toGregorianCalendar().getTimeInMillis()));
×
355
                    }
356
                }
×
357
            }
358
        }
359
        return window;
×
360
    }
361

362
    /**
363
     * Derives the set of shard repos the loader is expected to have populated for
364
     * a nanopub, from its admin metadata: {@code meta}, {@code full}/{@code text}
365
     * (feature-flagged), the signer's pubkey repo, one type repo per eligible
366
     * computed type (same exclusions as {@link NanopubLoader#executeLoading}:
367
     * locally-minted IRIs and non-http(s) IRIs are skipped), and {@code spaces}
368
     * for trigger-typed nanopubs. The driver repo itself is excluded — membership
369
     * there is what put the nanopub in the window. {@code last30d} is not checked
370
     * (expiring content).
371
     */
372
    static Set<String> expectedShardRepos(IRI npId, String pubkeyHash, Set<IRI> types, String driverRepo) {
373
        Set<String> repos = new LinkedHashSet<>();
12✔
374
        repos.add("meta");
12✔
375
        if (FeatureFlags.fullRepoEnabled()) {
6!
376
            repos.add("full");
12✔
377
        }
378
        if (FeatureFlags.textRepoEnabled()) {
6!
379
            repos.add("text");
12✔
380
        }
381
        repos.add("pubkey_" + pubkeyHash);
15✔
382
        for (IRI typeIri : types) {
30✔
383
            if (typeIri.stringValue().startsWith(npId.stringValue())) {
18✔
384
                continue;
3✔
385
            }
386
            if (!typeIri.stringValue().matches("https?://.*")) {
15✔
387
                continue;
3✔
388
            }
389
            repos.add("type_" + Utils.createHash(typeIri));
18✔
390
        }
3✔
391
        if (FeatureFlags.spacesEnabled() && SpacesExtractor.isSpaceRelevant(types)) {
15!
392
            repos.add("spaces");
12✔
393
        }
394
        repos.remove(driverRepo);
12✔
395
        return repos;
6✔
396
    }
397

398
    /**
399
     * Re-loads a nanopub with missing shards: reconstructs it from the driver
400
     * repo's four content graphs and re-runs the full (idempotent) load, then
401
     * re-verifies the shards that were missing. A {@code spaces} shard that is
402
     * still missing after a successful re-load means the trigger-type proxy
403
     * over-approximated (empty extraction) — logged and treated as consistent.
404
     *
405
     * @return true if all missing shards are accounted for afterwards
406
     */
407
    private static boolean repairNanopub(String driverRepo, IRI npId, List<String> missing) {
408
        logger.warn("Nanopub <{}> is missing from shard repo(s) {}; re-loading (see issue #139)", npId, missing);
×
409
        Nanopub np;
410
        try {
411
            np = reconstructNanopub(driverRepo, npId);
×
412
        } catch (Exception ex) {
×
413
            logger.warn("Could not reconstruct nanopub <{}> from repo '{}': {}", npId, driverRepo, ex.getMessage(), ex);
×
414
            return false;
×
415
        }
×
416
        try {
417
            NanopubLoader.load(np, -1);
×
418
        } catch (Exception ex) {
×
419
            logger.warn("Re-load of nanopub <{}> failed: {}", npId, ex.getMessage(), ex);
×
420
            return false;
×
421
        }
×
422
        boolean allRepaired = true;
×
423
        for (String repo : missing) {
×
424
            if (fetchPresentNanopubs(repo, List.of(npId)).contains(npId)) {
×
425
                logger.warn("Repaired: nanopub <{}> re-loaded into shard repo '{}'", npId, repo);
×
426
            } else if ("spaces".equals(repo)) {
×
427
                logger.info("Nanopub <{}> still absent from 'spaces' after re-load; its extraction is empty, treating as consistent", npId);
×
428
            } else {
429
                logger.warn("Nanopub <{}> still missing from shard repo '{}' after re-load", npId, repo);
×
430
                allRepaired = false;
×
431
            }
432
        }
×
433
        return allRepaired;
×
434
    }
435

436
    /**
437
     * Rebuilds a {@link Nanopub} object from the four content graphs stored in the
438
     * given repo, discovered via {@code <npId> npa:hasGraph ?g} in {@code npa:graph}.
439
     */
440
    static Nanopub reconstructNanopub(String repoName, IRI npId) throws MalformedNanopubException {
441
        List<Statement> content = new ArrayList<>();
12✔
442
        try (RepositoryConnection conn = TripleStore.get().getRepoConnection(repoName)) {
12✔
443
            List<IRI> graphs = new ArrayList<>();
12✔
444
            try (RepositoryResult<Statement> r = conn.getStatements(npId, NPA.HAS_GRAPH, null, NPA.GRAPH)) {
36✔
445
                while (r.hasNext()) {
9✔
446
                    Value o = r.next().getObject();
15✔
447
                    if (o instanceof IRI iri) {
18!
448
                        graphs.add(iri);
12✔
449
                    }
450
                }
3✔
451
            }
452
            for (IRI g : graphs) {
30✔
453
                try (RepositoryResult<Statement> r = conn.getStatements(null, null, null, g)) {
36✔
454
                    while (r.hasNext()) {
9✔
455
                        content.add(r.next());
21✔
456
                    }
457
                }
458
            }
3✔
459
        }
460
        return new NanopubImpl(content);
15✔
461
    }
462

463
    private record WindowEntry(IRI npId, long loadNumber, long loadedAtMs) {
×
464
    }
465

466
    private record NanopubShardInfo(String pubkeyHash, Set<IRI> types) {
×
467
    }
468

469
    /**
470
     * Fetches the next batch of nanopubs to verify: load number above the
471
     * checkpoint, ordered by load number, capped at {@link #MAX_NPS_PER_TICK}.
472
     * No age filter here — completion gating against {@code meta} (or the
473
     * {@link #GRACE_MS} patience for meta-pending nanopubs) happens in the
474
     * sweep loop, where it can halt the checkpoint precisely.
475
     */
476
    private static List<WindowEntry> fetchWindow(String driverRepo, long checkpoint) {
477
        List<WindowEntry> window = new ArrayList<>();
×
478
        try (RepositoryConnection conn = TripleStore.get().getRepoConnection(driverRepo)) {
×
479
            TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL,
×
480
                    "SELECT ?np ?ln ?ts WHERE { graph <" + NPA.GRAPH + "> { "
481
                    + "?np <" + NPA.HAS_LOAD_NUMBER + "> ?ln ; <" + NPA.HAS_LOAD_TIMESTAMP + "> ?ts . "
482
                    + "FILTER (?ln > " + checkpoint + ") "
483
                    + "} } ORDER BY ?ln LIMIT " + MAX_NPS_PER_TICK);
484
            try (TupleQueryResult r = q.evaluate()) {
×
485
                while (r.hasNext()) {
×
486
                    BindingSet b = r.next();
×
487
                    if (b.getValue("np") instanceof IRI npId && b.getValue("ts") instanceof Literal tsLit) {
×
488
                        window.add(new WindowEntry(npId, Long.parseLong(b.getValue("ln").stringValue()),
×
489
                                tsLit.calendarValue().toGregorianCalendar().getTimeInMillis()));
×
490
                    }
491
                }
×
492
            }
493
        }
494
        return window;
×
495
    }
496

497
    /**
498
     * Fetches pubkey hash and computed types for the window's nanopubs from the
499
     * driver repo's admin graph in one query.
500
     */
501
    private static Map<IRI, NanopubShardInfo> fetchShardInfos(String driverRepo, List<WindowEntry> window) {
502
        Map<IRI, NanopubShardInfo> result = new HashMap<>();
×
503
        try (RepositoryConnection conn = TripleStore.get().getRepoConnection(driverRepo)) {
×
504
            TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL,
×
505
                    "SELECT ?np ?ph ?t WHERE { graph <" + NPA.GRAPH + "> { "
506
                    + valuesClause(window.stream().map(WindowEntry::npId).toList())
×
507
                    + "?np <" + NPA.HAS_VALID_SIGNATURE_FOR_PUBLIC_KEY_HASH + "> ?ph . "
508
                    + "OPTIONAL { ?np <" + NPX.HAS_NANOPUB_TYPE + "> ?t } "
509
                    + "} }");
510
            try (TupleQueryResult r = q.evaluate()) {
×
511
                while (r.hasNext()) {
×
512
                    BindingSet b = r.next();
×
513
                    if (!(b.getValue("np") instanceof IRI npId)) {
×
514
                        continue;
515
                    }
516
                    NanopubShardInfo info = result.computeIfAbsent(npId,
×
517
                            k -> new NanopubShardInfo(b.getValue("ph").stringValue(), new LinkedHashSet<>()));
×
518
                    if (b.getValue("t") instanceof IRI typeIri) {
×
519
                        info.types().add(typeIri);
×
520
                    }
521
                }
×
522
            }
523
        }
524
        return result;
×
525
    }
526

527
    /**
528
     * Returns which of the given nanopubs carry an {@code npa:hasLoadNumber} stamp
529
     * in the given repo's admin graph.
530
     */
531
    private static Set<IRI> fetchPresentNanopubs(String repoName, List<IRI> npIds) {
532
        Set<IRI> present = new HashSet<>();
×
533
        try (RepositoryConnection conn = TripleStore.get().getRepoConnection(repoName)) {
×
534
            TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL,
×
535
                    "SELECT ?np WHERE { graph <" + NPA.GRAPH + "> { "
536
                    + valuesClause(npIds)
×
537
                    + "?np <" + NPA.HAS_LOAD_NUMBER + "> ?ln . "
538
                    + "} }");
539
            try (TupleQueryResult r = q.evaluate()) {
×
540
                while (r.hasNext()) {
×
541
                    BindingSet b = r.next();
×
542
                    if (b.getValue("np") instanceof IRI npId) {
×
543
                        present.add(npId);
×
544
                    }
545
                }
×
546
            }
547
        }
548
        return present;
×
549
    }
550

551
    private static String valuesClause(List<IRI> npIds) {
552
        StringBuilder sb = new StringBuilder("VALUES ?np { ");
×
553
        for (IRI npId : npIds) {
×
554
            sb.append("<").append(npId.stringValue()).append("> ");
×
555
        }
×
556
        sb.append("} ");
×
557
        return sb.toString();
×
558
    }
559

560
    private static long fetchMaxLoadNumber(String repoName) {
561
        try (RepositoryConnection conn = TripleStore.get().getRepoConnection(repoName)) {
×
562
            TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL,
×
563
                    "SELECT (MAX(?ln) AS ?maxLn) WHERE { graph <" + NPA.GRAPH + "> { "
564
                    + "?np <" + NPA.HAS_LOAD_NUMBER + "> ?ln . } }");
565
            try (TupleQueryResult r = q.evaluate()) {
×
566
                if (r.hasNext()) {
×
567
                    Value v = r.next().getValue("maxLn");
×
568
                    if (v != null) {
×
569
                        return Long.parseLong(v.stringValue());
×
570
                    }
571
                }
572
            }
×
573
        }
×
574
        return -1;
×
575
    }
576

577
    /**
578
     * Reads the persisted checkpoint from the admin repo, or {@code null} if none
579
     * exists yet or it was recorded against a different driver repo.
580
     */
581
    private static Long readCheckpoint(String driverRepo) {
582
        try (RepositoryConnection conn = TripleStore.get().getAdminRepoConnection()) {
×
583
            Value driver = Utils.getObjectForPattern(conn, NPA.GRAPH, NPA.THIS_REPO, HAS_RECONCILIATION_DRIVER);
×
584
            Value cp = Utils.getObjectForPattern(conn, NPA.GRAPH, NPA.THIS_REPO, HAS_RECONCILIATION_CHECKPOINT);
×
585
            if (driver == null || cp == null || !driverRepo.equals(driver.stringValue())) {
×
586
                return null;
×
587
            }
588
            return Long.parseLong(cp.stringValue());
×
589
        } catch (NumberFormatException ex) {
×
590
            logger.warn("Malformed npa:hasReconciliationCheckpoint literal in admin repo: {}", ex.getMessage());
×
591
            return null;
×
592
        }
593
    }
594

595
    private static void persistCheckpoint(String driverRepo, long checkpoint) {
596
        try (RepositoryConnection conn = TripleStore.get().getAdminRepoConnection()) {
×
597
            conn.begin(IsolationLevels.SERIALIZABLE);
×
598
            conn.remove(NPA.THIS_REPO, HAS_RECONCILIATION_DRIVER, null, NPA.GRAPH);
×
599
            conn.remove(NPA.THIS_REPO, HAS_RECONCILIATION_CHECKPOINT, null, NPA.GRAPH);
×
600
            conn.add(NPA.THIS_REPO, HAS_RECONCILIATION_DRIVER, vf.createLiteral(driverRepo), NPA.GRAPH);
×
601
            conn.add(NPA.THIS_REPO, HAS_RECONCILIATION_CHECKPOINT, vf.createLiteral(checkpoint), NPA.GRAPH);
×
602
            conn.commit();
×
603
        }
604
    }
×
605

606
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc