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

knowledgepixels / nanopub-query / 30442040680

29 Jul 2026 10:01AM UTC coverage: 59.753% (-0.2%) from 60.0%
30442040680

Pull #141

github

web-flow
Merge 1fa1045cf into 5b02deab8
Pull Request #141: feat(reconciler): meta-stamp completion gate and 5-minute sweep cadence

620 of 1168 branches covered (53.08%)

Branch coverage included in aggregate %.

1800 of 2882 relevant lines covered (62.46%)

9.59 hits per line

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

18.87
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
     * Cumulative count of nanopubs whose shard fan-out was verified (clean or
105
     * repaired) since process start. Read by {@link MetricsCollector}.
106
     */
107
    static volatile long checkedNanopubCount = 0;
6✔
108

109
    /**
110
     * Cumulative count of shard repos found missing and successfully re-loaded
111
     * since process start. Read by {@link MetricsCollector}. Any non-zero value
112
     * is evidence of the backend acknowledging a non-durable write.
113
     */
114
    static volatile long repairedShardCount = 0;
9✔
115

116
    private ShardReconciler() {
117
    }
118

119
    /**
120
     * Runs one bounded reconciliation pass. Never throws: any failure is logged
121
     * and retried on the next tick (the checkpoint only advances past verified
122
     * nanopubs). No-op unless the reconciliation feature is enabled and the
123
     * service is READY.
124
     */
125
    public static void tick() {
126
        if (!FeatureFlags.reconciliationEnabled()) {
×
127
            return;
×
128
        }
129
        if (StatusController.get().getState().state != StatusController.State.READY) {
×
130
            return;
×
131
        }
132
        try {
133
            runTick();
×
134
        } catch (Exception ex) {
×
135
            logger.warn("Shard reconciliation tick failed; will retry on next tick: {}", ex.getMessage(), ex);
×
136
        }
×
137
    }
×
138

139
    private static void runTick() {
140
        String driverRepo = FeatureFlags.fullRepoEnabled() ? "full" : "meta";
×
141

142
        Long checkpoint = readCheckpoint(driverRepo);
×
143
        if (checkpoint == null) {
×
144
            // First run (or driver repo changed): establish the baseline at the
145
            // driver's current max load number. Only nanopubs loaded after this
146
            // point are swept.
147
            long max = fetchMaxLoadNumber(driverRepo);
×
148
            persistCheckpoint(driverRepo, max);
×
149
            logger.info("Shard reconciliation checkpoint initialized at load number {} of repo '{}'", max, driverRepo);
×
150
            return;
×
151
        }
152

153
        List<WindowEntry> window = fetchWindow(driverRepo, checkpoint);
×
154
        if (window.isEmpty()) {
×
155
            return;
×
156
        }
157
        Map<IRI, NanopubShardInfo> shardInfos = fetchShardInfos(driverRepo, window);
×
158

159
        // Batched membership lookup: one query per distinct shard repo, covering
160
        // all window nanopubs that expect it.
161
        Map<String, List<IRI>> npsByShard = new LinkedHashMap<>();
×
162
        for (WindowEntry e : window) {
×
163
            NanopubShardInfo info = shardInfos.get(e.npId());
×
164
            if (info == null) {
×
165
                continue;
×
166
            }
167
            for (String repo : expectedShardRepos(e.npId(), info.pubkeyHash(), info.types(), driverRepo)) {
×
168
                npsByShard.computeIfAbsent(repo, k -> new ArrayList<>()).add(e.npId());
×
169
            }
×
170
        }
×
171
        Map<String, Set<IRI>> presentByShard = new HashMap<>();
×
172
        for (Map.Entry<String, List<IRI>> e : npsByShard.entrySet()) {
×
173
            presentByShard.put(e.getKey(), fetchPresentNanopubs(e.getKey(), e.getValue()));
×
174
        }
×
175

176
        long newCheckpoint = checkpoint;
×
177
        long checked = 0;
×
178
        long repaired = 0;
×
179
        try {
180
            for (WindowEntry e : window) {
×
181
                // Completion gate: meta is written last, so a nanopub already in meta
182
                // has finished its fan-out and can be verified immediately regardless
183
                // of age. A nanopub not yet in meta is normally an in-flight load —
184
                // halt the sweep in front of it (checkpoint stays put, next tick
185
                // re-examines) until it either completes or exceeds the patience
186
                // window, after which the missing meta stamp is itself the incident
187
                // and falls through to the regular repair path.
188
                boolean completed = "meta".equals(driverRepo)
×
189
                        || presentByShard.getOrDefault("meta", Set.of()).contains(e.npId());
×
190
                if (!completed && System.currentTimeMillis() - e.loadedAtMs() < GRACE_MS) {
×
191
                    logger.debug("Halting shard sweep at <{}> (load number {}): not yet in meta, likely in flight", e.npId(), e.loadNumber());
×
192
                    return;
×
193
                }
194
                NanopubShardInfo info = shardInfos.get(e.npId());
×
195
                if (info == null) {
×
196
                    // No pubkey-hash admin triple in the driver repo; nothing we can
197
                    // derive shards from. Loud, but don't stall the sweep on it.
198
                    logger.warn("Skipping shard reconciliation for <{}>: no npa:hasValidSignatureForPublicKeyHash found in repo '{}'", e.npId(), driverRepo);
×
199
                    newCheckpoint = e.loadNumber();
×
200
                    continue;
×
201
                }
202
                List<String> missing = new ArrayList<>();
×
203
                for (String repo : expectedShardRepos(e.npId(), info.pubkeyHash(), info.types(), driverRepo)) {
×
204
                    if (!presentByShard.getOrDefault(repo, Set.of()).contains(e.npId())) {
×
205
                        missing.add(repo);
×
206
                    }
207
                }
×
208
                if (!missing.isEmpty()) {
×
209
                    if (!repairNanopub(driverRepo, e.npId(), missing)) {
×
210
                        // Repair failed: stop here so the checkpoint stays before this
211
                        // nanopub and the next tick retries.
212
                        return;
×
213
                    }
214
                    repaired += missing.size();
×
215
                }
216
                newCheckpoint = e.loadNumber();
×
217
                checked++;
×
218
            }
×
219
        } finally {
220
            checkedNanopubCount += checked;
×
221
            repairedShardCount += repaired;
×
222
            if (newCheckpoint > checkpoint) {
×
223
                persistCheckpoint(driverRepo, newCheckpoint);
×
224
            }
225
        }
226
        if (repaired > 0) {
×
227
            logger.warn("Shard reconciliation repaired {} missing shard(s) across {} nanopub(s); the backend acknowledged non-durable writes (see issue #139)", repaired, checked);
×
228
        } else {
229
            logger.debug("Shard reconciliation verified {} nanopub(s) up to load number {} of repo '{}'", checked, newCheckpoint, driverRepo);
×
230
        }
231
    }
×
232

233
    /**
234
     * Derives the set of shard repos the loader is expected to have populated for
235
     * a nanopub, from its admin metadata: {@code meta}, {@code full}/{@code text}
236
     * (feature-flagged), the signer's pubkey repo, one type repo per eligible
237
     * computed type (same exclusions as {@link NanopubLoader#executeLoading}:
238
     * locally-minted IRIs and non-http(s) IRIs are skipped), and {@code spaces}
239
     * for trigger-typed nanopubs. The driver repo itself is excluded — membership
240
     * there is what put the nanopub in the window. {@code last30d} is not checked
241
     * (expiring content).
242
     */
243
    static Set<String> expectedShardRepos(IRI npId, String pubkeyHash, Set<IRI> types, String driverRepo) {
244
        Set<String> repos = new LinkedHashSet<>();
12✔
245
        repos.add("meta");
12✔
246
        if (FeatureFlags.fullRepoEnabled()) {
6!
247
            repos.add("full");
12✔
248
        }
249
        if (FeatureFlags.textRepoEnabled()) {
6!
250
            repos.add("text");
12✔
251
        }
252
        repos.add("pubkey_" + pubkeyHash);
15✔
253
        for (IRI typeIri : types) {
30✔
254
            if (typeIri.stringValue().startsWith(npId.stringValue())) {
18✔
255
                continue;
3✔
256
            }
257
            if (!typeIri.stringValue().matches("https?://.*")) {
15✔
258
                continue;
3✔
259
            }
260
            repos.add("type_" + Utils.createHash(typeIri));
18✔
261
        }
3✔
262
        if (FeatureFlags.spacesEnabled() && SpacesExtractor.isSpaceRelevant(types)) {
15!
263
            repos.add("spaces");
12✔
264
        }
265
        repos.remove(driverRepo);
12✔
266
        return repos;
6✔
267
    }
268

269
    /**
270
     * Re-loads a nanopub with missing shards: reconstructs it from the driver
271
     * repo's four content graphs and re-runs the full (idempotent) load, then
272
     * re-verifies the shards that were missing. A {@code spaces} shard that is
273
     * still missing after a successful re-load means the trigger-type proxy
274
     * over-approximated (empty extraction) — logged and treated as consistent.
275
     *
276
     * @return true if all missing shards are accounted for afterwards
277
     */
278
    private static boolean repairNanopub(String driverRepo, IRI npId, List<String> missing) {
279
        logger.warn("Nanopub <{}> is missing from shard repo(s) {}; re-loading (see issue #139)", npId, missing);
×
280
        Nanopub np;
281
        try {
282
            np = reconstructNanopub(driverRepo, npId);
×
283
        } catch (Exception ex) {
×
284
            logger.warn("Could not reconstruct nanopub <{}> from repo '{}': {}", npId, driverRepo, ex.getMessage(), ex);
×
285
            return false;
×
286
        }
×
287
        try {
288
            NanopubLoader.load(np, -1);
×
289
        } catch (Exception ex) {
×
290
            logger.warn("Re-load of nanopub <{}> failed: {}", npId, ex.getMessage(), ex);
×
291
            return false;
×
292
        }
×
293
        boolean allRepaired = true;
×
294
        for (String repo : missing) {
×
295
            if (fetchPresentNanopubs(repo, List.of(npId)).contains(npId)) {
×
296
                logger.warn("Repaired: nanopub <{}> re-loaded into shard repo '{}'", npId, repo);
×
297
            } else if ("spaces".equals(repo)) {
×
298
                logger.info("Nanopub <{}> still absent from 'spaces' after re-load; its extraction is empty, treating as consistent", npId);
×
299
            } else {
300
                logger.warn("Nanopub <{}> still missing from shard repo '{}' after re-load", npId, repo);
×
301
                allRepaired = false;
×
302
            }
303
        }
×
304
        return allRepaired;
×
305
    }
306

307
    /**
308
     * Rebuilds a {@link Nanopub} object from the four content graphs stored in the
309
     * given repo, discovered via {@code <npId> npa:hasGraph ?g} in {@code npa:graph}.
310
     */
311
    static Nanopub reconstructNanopub(String repoName, IRI npId) throws MalformedNanopubException {
312
        List<Statement> content = new ArrayList<>();
12✔
313
        try (RepositoryConnection conn = TripleStore.get().getRepoConnection(repoName)) {
12✔
314
            List<IRI> graphs = new ArrayList<>();
12✔
315
            try (RepositoryResult<Statement> r = conn.getStatements(npId, NPA.HAS_GRAPH, null, NPA.GRAPH)) {
36✔
316
                while (r.hasNext()) {
9✔
317
                    Value o = r.next().getObject();
15✔
318
                    if (o instanceof IRI iri) {
18!
319
                        graphs.add(iri);
12✔
320
                    }
321
                }
3✔
322
            }
323
            for (IRI g : graphs) {
30✔
324
                try (RepositoryResult<Statement> r = conn.getStatements(null, null, null, g)) {
36✔
325
                    while (r.hasNext()) {
9✔
326
                        content.add(r.next());
21✔
327
                    }
328
                }
329
            }
3✔
330
        }
331
        return new NanopubImpl(content);
15✔
332
    }
333

334
    private record WindowEntry(IRI npId, long loadNumber, long loadedAtMs) {
×
335
    }
336

337
    private record NanopubShardInfo(String pubkeyHash, Set<IRI> types) {
×
338
    }
339

340
    /**
341
     * Fetches the next batch of nanopubs to verify: load number above the
342
     * checkpoint, ordered by load number, capped at {@link #MAX_NPS_PER_TICK}.
343
     * No age filter here — completion gating against {@code meta} (or the
344
     * {@link #GRACE_MS} patience for meta-pending nanopubs) happens in the
345
     * sweep loop, where it can halt the checkpoint precisely.
346
     */
347
    private static List<WindowEntry> fetchWindow(String driverRepo, long checkpoint) {
348
        List<WindowEntry> window = new ArrayList<>();
×
349
        try (RepositoryConnection conn = TripleStore.get().getRepoConnection(driverRepo)) {
×
350
            TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL,
×
351
                    "SELECT ?np ?ln ?ts WHERE { graph <" + NPA.GRAPH + "> { "
352
                    + "?np <" + NPA.HAS_LOAD_NUMBER + "> ?ln ; <" + NPA.HAS_LOAD_TIMESTAMP + "> ?ts . "
353
                    + "FILTER (?ln > " + checkpoint + ") "
354
                    + "} } ORDER BY ?ln LIMIT " + MAX_NPS_PER_TICK);
355
            try (TupleQueryResult r = q.evaluate()) {
×
356
                while (r.hasNext()) {
×
357
                    BindingSet b = r.next();
×
358
                    if (b.getValue("np") instanceof IRI npId && b.getValue("ts") instanceof Literal tsLit) {
×
359
                        window.add(new WindowEntry(npId, Long.parseLong(b.getValue("ln").stringValue()),
×
360
                                tsLit.calendarValue().toGregorianCalendar().getTimeInMillis()));
×
361
                    }
362
                }
×
363
            }
364
        }
365
        return window;
×
366
    }
367

368
    /**
369
     * Fetches pubkey hash and computed types for the window's nanopubs from the
370
     * driver repo's admin graph in one query.
371
     */
372
    private static Map<IRI, NanopubShardInfo> fetchShardInfos(String driverRepo, List<WindowEntry> window) {
373
        Map<IRI, NanopubShardInfo> result = new HashMap<>();
×
374
        try (RepositoryConnection conn = TripleStore.get().getRepoConnection(driverRepo)) {
×
375
            TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL,
×
376
                    "SELECT ?np ?ph ?t WHERE { graph <" + NPA.GRAPH + "> { "
377
                    + valuesClause(window.stream().map(WindowEntry::npId).toList())
×
378
                    + "?np <" + NPA.HAS_VALID_SIGNATURE_FOR_PUBLIC_KEY_HASH + "> ?ph . "
379
                    + "OPTIONAL { ?np <" + NPX.HAS_NANOPUB_TYPE + "> ?t } "
380
                    + "} }");
381
            try (TupleQueryResult r = q.evaluate()) {
×
382
                while (r.hasNext()) {
×
383
                    BindingSet b = r.next();
×
384
                    if (!(b.getValue("np") instanceof IRI npId)) {
×
385
                        continue;
386
                    }
387
                    NanopubShardInfo info = result.computeIfAbsent(npId,
×
388
                            k -> new NanopubShardInfo(b.getValue("ph").stringValue(), new LinkedHashSet<>()));
×
389
                    if (b.getValue("t") instanceof IRI typeIri) {
×
390
                        info.types().add(typeIri);
×
391
                    }
392
                }
×
393
            }
394
        }
395
        return result;
×
396
    }
397

398
    /**
399
     * Returns which of the given nanopubs carry an {@code npa:hasLoadNumber} stamp
400
     * in the given repo's admin graph.
401
     */
402
    private static Set<IRI> fetchPresentNanopubs(String repoName, List<IRI> npIds) {
403
        Set<IRI> present = new HashSet<>();
×
404
        try (RepositoryConnection conn = TripleStore.get().getRepoConnection(repoName)) {
×
405
            TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL,
×
406
                    "SELECT ?np WHERE { graph <" + NPA.GRAPH + "> { "
407
                    + valuesClause(npIds)
×
408
                    + "?np <" + NPA.HAS_LOAD_NUMBER + "> ?ln . "
409
                    + "} }");
410
            try (TupleQueryResult r = q.evaluate()) {
×
411
                while (r.hasNext()) {
×
412
                    BindingSet b = r.next();
×
413
                    if (b.getValue("np") instanceof IRI npId) {
×
414
                        present.add(npId);
×
415
                    }
416
                }
×
417
            }
418
        }
419
        return present;
×
420
    }
421

422
    private static String valuesClause(List<IRI> npIds) {
423
        StringBuilder sb = new StringBuilder("VALUES ?np { ");
×
424
        for (IRI npId : npIds) {
×
425
            sb.append("<").append(npId.stringValue()).append("> ");
×
426
        }
×
427
        sb.append("} ");
×
428
        return sb.toString();
×
429
    }
430

431
    private static long fetchMaxLoadNumber(String repoName) {
432
        try (RepositoryConnection conn = TripleStore.get().getRepoConnection(repoName)) {
×
433
            TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL,
×
434
                    "SELECT (MAX(?ln) AS ?maxLn) WHERE { graph <" + NPA.GRAPH + "> { "
435
                    + "?np <" + NPA.HAS_LOAD_NUMBER + "> ?ln . } }");
436
            try (TupleQueryResult r = q.evaluate()) {
×
437
                if (r.hasNext()) {
×
438
                    Value v = r.next().getValue("maxLn");
×
439
                    if (v != null) {
×
440
                        return Long.parseLong(v.stringValue());
×
441
                    }
442
                }
443
            }
×
444
        }
×
445
        return -1;
×
446
    }
447

448
    /**
449
     * Reads the persisted checkpoint from the admin repo, or {@code null} if none
450
     * exists yet or it was recorded against a different driver repo.
451
     */
452
    private static Long readCheckpoint(String driverRepo) {
453
        try (RepositoryConnection conn = TripleStore.get().getAdminRepoConnection()) {
×
454
            Value driver = Utils.getObjectForPattern(conn, NPA.GRAPH, NPA.THIS_REPO, HAS_RECONCILIATION_DRIVER);
×
455
            Value cp = Utils.getObjectForPattern(conn, NPA.GRAPH, NPA.THIS_REPO, HAS_RECONCILIATION_CHECKPOINT);
×
456
            if (driver == null || cp == null || !driverRepo.equals(driver.stringValue())) {
×
457
                return null;
×
458
            }
459
            return Long.parseLong(cp.stringValue());
×
460
        } catch (NumberFormatException ex) {
×
461
            logger.warn("Malformed npa:hasReconciliationCheckpoint literal in admin repo: {}", ex.getMessage());
×
462
            return null;
×
463
        }
464
    }
465

466
    private static void persistCheckpoint(String driverRepo, long checkpoint) {
467
        try (RepositoryConnection conn = TripleStore.get().getAdminRepoConnection()) {
×
468
            conn.begin(IsolationLevels.SERIALIZABLE);
×
469
            conn.remove(NPA.THIS_REPO, HAS_RECONCILIATION_DRIVER, null, NPA.GRAPH);
×
470
            conn.remove(NPA.THIS_REPO, HAS_RECONCILIATION_CHECKPOINT, null, NPA.GRAPH);
×
471
            conn.add(NPA.THIS_REPO, HAS_RECONCILIATION_DRIVER, vf.createLiteral(driverRepo), NPA.GRAPH);
×
472
            conn.add(NPA.THIS_REPO, HAS_RECONCILIATION_CHECKPOINT, vf.createLiteral(checkpoint), NPA.GRAPH);
×
473
            conn.commit();
×
474
        }
475
    }
×
476

477
}
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