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

knowledgepixels / nanopub-query / 26774597310

01 Jun 2026 06:41PM UTC coverage: 58.598% (-1.0%) from 59.597%
26774597310

push

github

web-flow
Merge pull request #118 from knowledgepixels/fix/issue-117-getenvstring-subprocess

fix(env): read environment via System.getenv instead of forking a subprocess (#117)

471 of 896 branches covered (52.57%)

Branch coverage included in aggregate %.

1359 of 2227 relevant lines covered (61.02%)

9.34 hits per line

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

63.64
src/main/java/com/knowledgepixels/query/TripleStore.java
1
package com.knowledgepixels.query;
2

3
import org.apache.http.client.config.RequestConfig;
4
import org.apache.http.client.methods.CloseableHttpResponse;
5
import org.apache.http.client.methods.HttpUriRequest;
6
import org.apache.http.client.methods.RequestBuilder;
7
import org.apache.http.entity.StringEntity;
8
import org.apache.http.impl.client.BasicResponseHandler;
9
import org.apache.http.impl.client.CloseableHttpClient;
10
import org.apache.http.impl.client.HttpClients;
11
import org.eclipse.rdf4j.common.transaction.IsolationLevels;
12
import org.eclipse.rdf4j.model.ValueFactory;
13
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
14
import org.eclipse.rdf4j.repository.Repository;
15
import org.eclipse.rdf4j.repository.RepositoryConnection;
16
import org.eclipse.rdf4j.repository.base.RepositoryConnectionWrapper;
17
import org.eclipse.rdf4j.repository.http.HTTPRepository;
18
import org.nanopub.NanopubUtils;
19
import org.nanopub.vocabulary.NPA;
20
import org.slf4j.Logger;
21
import org.slf4j.LoggerFactory;
22

23
import java.io.BufferedReader;
24
import java.io.IOException;
25
import java.io.InputStreamReader;
26
import java.util.*;
27
import java.util.Map.Entry;
28
import java.util.concurrent.ConcurrentHashMap;
29
import java.util.concurrent.TimeUnit;
30
import java.util.concurrent.atomic.AtomicBoolean;
31
import java.util.concurrent.atomic.AtomicInteger;
32
import java.util.concurrent.locks.ReadWriteLock;
33
import java.util.concurrent.locks.ReentrantReadWriteLock;
34

35
/**
36
 * Class to access the database in the form of triple stores.
37
 */
38
public class TripleStore {
39

40
    /**
41
     * Name of the admin graph.
42
     */
43
    public static final String ADMIN_REPO = "admin";
44

45
    private static ValueFactory vf = SimpleValueFactory.getInstance();
6✔
46

47
    private static final Logger log = LoggerFactory.getLogger(TripleStore.class);
12✔
48

49
    private final Map<String, Repository> repositories = new LinkedHashMap<>();
15✔
50

51
    /**
52
     * Per-repo open-connection counter, read by the eviction loop to skip repos that
53
     * still have live connections. Incremented in {@link #getRepoConnection(String)}
54
     * just before the connection is handed out and decremented via a
55
     * {@link RepositoryConnectionWrapper} that intercepts {@code close()} exactly once.
56
     */
57
    private final ConcurrentHashMap<String, AtomicInteger> openConnections = new ConcurrentHashMap<>();
15✔
58

59
    private String endpointBase = null;
9✔
60
    private String endpointType = null;
9✔
61

62
    private static TripleStore tripleStoreInstance;
63

64
    /**
65
     * Returns singleton triple store instance.
66
     *
67
     * @return Triple store instance
68
     */
69
    public static TripleStore get() {
70
        if (tripleStoreInstance == null) {
6!
71
            try {
72
                tripleStoreInstance = new TripleStore();
×
73
            } catch (IOException ex) {
×
74
                log.info("Could not init TripleStore. ", ex);
×
75
            }
×
76
        }
77
        return tripleStoreInstance;
×
78
    }
79

80
    private TripleStore() throws IOException {
6✔
81
        // Read directly from the JVM's in-memory environment block rather than via
82
        // Apache Commons Exec's EnvironmentUtils.getProcEnvironment(), which forks a
83
        // subprocess and can throw under memory pressure / a restart storm — leaving
84
        // the singleton uninitialised. Same fragile mechanism that caused issue #117
85
        // on the per-nanopub hot path (see Utils.getEnvString); retired here too.
86
        endpointBase = System.getenv("ENDPOINT_BASE");
12✔
87
        log.info("Endpoint base: {}", endpointBase);
15✔
88
        endpointType = System.getenv("ENDPOINT_TYPE");
12✔
89

90
        getRepository("empty");  // Make sure empty repo exists
×
91
    }
×
92

93
    /**
94
     * Shared HTTP client for all RDF4J traffic. Apache HttpClient treats all requests
95
     * to a given host + port + protocol as one "route", so every `HTTPRepository` in
96
     * this process funnels through this single client's connection pool — plus the
97
     * two ad-hoc users in {@link #createRepo} and {@link #getRepositoryNames}, which
98
     * have been consolidated onto this client too.
99
     *
100
     * <p>The Apache defaults (maxPerRoute=2 / maxTotal=20) throttle four concurrent
101
     * loader-pool threads down to two-way parallelism at the HTTP layer — invisible
102
     * client-side serialisation the code is actively fighting. Raised to 10 / 40
103
     * here: comfortable headroom for the 4-thread loader pool plus admin-repo
104
     * transactions and the metrics tick, small enough to be a conservative first
105
     * step with room to grow later.
106
     *
107
     * <p>Timeouts set via {@code setDefaultRequestConfig}:
108
     * <ul>
109
     *   <li><b>socket-read = 60 s</b> — an individual HTTP response body must arrive
110
     *       within this window. Healthy per-nanopub commits complete in milliseconds,
111
     *       so 60 s is pure safety margin; its job is to turn the silent "threads
112
     *       parked forever inside a commit" wedge (observed in the April test) into
113
     *       a recoverable error that feeds the existing retry path.</li>
114
     *   <li><b>connection-request = 30 s</b> — a caller waiting for a pooled connection
115
     *       gives up after this long. Prevents the invisible self-deadlock in
116
     *       {@code loadInvalidateStatements} (one thread holding N connections while
117
     *       waiting for an (N+1)th) from hanging forever.</li>
118
     *   <li><b>connect = 10 s</b> — kills TCP handshakes that stall. Generous but
119
     *       bounded.</li>
120
     * </ul>
121
     * Without these defaults, HttpClient uses {@code -1} everywhere, which means
122
     * "wait forever".
123
     */
124
    private final CloseableHttpClient httpclient = HttpClients.custom()
9✔
125
            .setMaxConnPerRoute(10)
6✔
126
            .setMaxConnTotal(40)
3✔
127
            .setDefaultRequestConfig(RequestConfig.custom()
9✔
128
                    .setSocketTimeout(60_000)
6✔
129
                    .setConnectionRequestTimeout(30_000)
6✔
130
                    .setConnectTimeout(10_000)
3✔
131
                    .build())
3✔
132
            // Hygiene: kill pooled connections that RDF4J has quietly closed server-side
133
            // before we try to reuse them. Without this, a half-broken connection is
134
            // only noticed when the next request fails, spending the full socket-read
135
            // timeout discovering it.
136
            .evictExpiredConnections()
9✔
137
            .evictIdleConnections(30, TimeUnit.SECONDS)
3✔
138
            .build();
6✔
139

140
    @GeneratedFlagForDependentElements
141
    Repository getRepository(String name) {
142
        synchronized (this) {
143
            if (repositories.size() > 100) {
144
                evictIdleRepos();
145
            }
146
            if (repositories.containsKey(name)) {
147
                // Move to the end of the list:
148
                Repository repo = repositories.remove(name);
149
                repositories.put(name, repo);
150
            } else {
151
                Repository repository = null;
152
                if (endpointType == null || endpointType.equals("rdf4j")) {
153
                    HTTPRepository hr = new HTTPRepository(endpointBase + "repositories/" + name);
154
                    hr.setHttpClient(httpclient);
155
                    repository = hr;
156
//                        } else if (endpointType.equals("virtuoso")) {
157
//                                repository = new VirtuosoRepository(endpointBase + name, username, password);
158
                } else {
159
                    throw new RuntimeException("Unknown repository type: " + endpointType);
160
                }
161
                repositories.put(name, repository);
162
                createRepo(name);
163
                getRepoConnection(name).close();
164
            }
165
            return repositories.get(name);
166
        }
167
    }
168

169
    /**
170
     * Return the repository connection for the given repository name.
171
     *
172
     * @param name repository name
173
     * @return repository connection
174
     */
175
    @GeneratedFlagForDependentElements
176
    public RepositoryConnection getRepoConnection(String name) {
177
        // The increment has to happen under the same monitor that guards eviction,
178
        // otherwise another thread could evict this repo in the window between
179
        // getRepository() returning and the counter going above zero. getConnection()
180
        // on HTTPRepository is local (it doesn't do HTTP), so holding the lock here
181
        // is cheap.
182
        synchronized (this) {
183
            Repository repo = getRepository(name);
184
            if (repo == null) {
185
                return null;
186
            }
187
            AtomicInteger counter = openConnections.computeIfAbsent(name, k -> new AtomicInteger());
×
188
            counter.incrementAndGet();
189
            try {
190
                return new CountingRepositoryConnection(repo, repo.getConnection(), counter);
191
            } catch (Throwable t) {
192
                // If getConnection() or the wrapper constructor throws after we bumped
193
                // the counter, decrement so the repo isn't pinned against eviction by
194
                // a phantom "active" connection it doesn't actually have.
195
                counter.decrementAndGet();
196
                throw t;
197
            }
198
        }
199
    }
200

201
    /**
202
     * Evicts the eldest cache entries until either the size is back within the
203
     * 100-entry cap or every remaining entry has at least one open connection.
204
     * The cap is load-bearing — each cached entry keeps an LMDB environment alive
205
     * on the RDF4J server (in-memory cache, mmap pages, native memory), so
206
     * exceeding it by much risks server-side OOM. Actively-used repos are skipped
207
     * rather than shut down, because {@link org.eclipse.rdf4j.repository.http.HTTPRepository#shutDown()}
208
     * closes the session manager and kills any live transaction on that repo with
209
     * a connection-close error (the {@code MMapIndexInput – Already closed}
210
     * failure mode observed on query-3 in the April test). The cache self-converges
211
     * as active repos become idle.
212
     */
213
    @GeneratedFlagForDependentElements
214
    private void evictIdleRepos() {
215
        List<String> skipped = new ArrayList<>();
216
        Iterator<Entry<String, Repository>> iter = repositories.entrySet().iterator();
217
        while (iter.hasNext() && repositories.size() > 100) {
218
            Entry<String, Repository> e = iter.next();
219
            AtomicInteger active = openConnections.get(e.getKey());
220
            if (active != null && active.get() > 0) {
221
                skipped.add(e.getKey());
222
                continue;
223
            }
224
            iter.remove();
225
            log.info("Shutting down repo: {}", e.getKey());
226
            e.getValue().shutDown();
227
            log.info("Shutdown complete");
228
        }
229
        if (!skipped.isEmpty()) {
230
            log.warn("Skipped eviction for {} active repo(s); cache size is now {} (cap 100). Active names: {}",
231
                    skipped.size(), repositories.size(), skipped);
232
        }
233
    }
234

235
    /**
236
     * Minimal wrapper around a delegate {@link RepositoryConnection} whose only job
237
     * is to decrement the per-repo open-connection counter exactly once when the
238
     * caller closes it. Uses {@link AtomicBoolean} so that repeated/idempotent
239
     * {@code close()} calls (common with try-with-resources plus explicit close)
240
     * don't decrement more than once.
241
     */
242
    private static final class CountingRepositoryConnection extends RepositoryConnectionWrapper {
243

244
        private final AtomicInteger counter;
245
        private final AtomicBoolean closed = new AtomicBoolean();
×
246

247
        CountingRepositoryConnection(Repository repo, RepositoryConnection delegate, AtomicInteger counter) {
248
            super(repo, delegate);
×
249
            this.counter = counter;
×
250
        }
×
251

252
        @Override
253
        public void close() {
254
            try {
255
                super.close();
×
256
            } finally {
257
                if (closed.compareAndSet(false, true)) {
×
258
                    counter.decrementAndGet();
×
259
                }
260
            }
261
        }
×
262

263
    }
264

265
    @GeneratedFlagForDependentElements
266
    private void createRepo(String repoName) {
267
        if (!repoName.equals(ADMIN_REPO)) {
268
            getRepository(ADMIN_REPO);  // make sure admin repo is loaded first
269
        }
270
        // Uses the shared this.httpclient rather than a per-call client so it inherits
271
        // the configured pool sizes (and, once change 1 of the fix plan lands, the
272
        // socket/connection-request timeouts).
273
        try {
274
            //log.info("Trying to creating repo " + name);
275

276
            // TODO new syntax somehow doesn't work for the Lucene case:
277

278
//                        String createRegularRepoQueryString = "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\n"
279
//                                        + "@prefix config: <tag:rdf4j.org,2023:config/>.\n"
280
//                                        + "[] a config:Repository ;\n"
281
//                                        + "    config:rep.id \"" + name + "\" ;\n"
282
//                                        + "    rdfs:label \"" + name + " native store\" ;\n"
283
//                                        + "    config:rep.impl [\n"
284
//                                        + "        config:rep.type \"openrdf:SailRepository\" ;\n"
285
//                                        + "        config:sail.impl [\n"
286
//                                        + "            config:sail.type \"openrdf:NativeStore\" ;\n"
287
//                                        + "            config:sail.iterationCacheSyncThreshold \"10000\";\n"
288
//                                        + "            config:native.tripleIndexes \"spoc,posc,ospc,opsc,psoc,sopc,spoc,cpos,cosp,cops,cpso,csop\" ;\n"
289
//                                        + "            config:sail.defaultQueryEvaluationMode \"STANDARD\"\n"
290
//                                        + "        ]\n"
291
//                                        + "    ].";
292
//                        String createTextRepoQueryString = "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\n"
293
//                                        + "@prefix config: <tag:rdf4j.org,2023:config/>.\n"
294
//                                        + "[] a config:Repository ;\n"
295
//                                        + "    config:rep.id \"" + name + "\" ;\n"
296
//                                        + "    rdfs:label \"" + name + " native store\" ;\n"
297
//                                        + "    config:rep.impl [\n"
298
//                                        + "        config:rep.type \"openrdf:SailRepository\" ;\n"
299
//                                        + "        config:sail.impl [\n"
300
//                                        + "            config:sail.type \"openrdf:LuceneSail\" ;\n"
301
//                                        + "            config:sail.lucene.indexDir \"index/\" ;\n"
302
//                                        + "            config:delegate [\n"
303
//                                        + "                config:rep.type \"openrdf:SailRepository\" ;\n"
304
//                                        + "                config:sail.impl [\n"
305
//                                        + "                    config:sail.type \"openrdf:NativeStore\" ;\n"
306
//                                        + "                    config:sail.iterationCacheSyncThreshold \"10000\";\n"
307
//                                        + "                    config:native.tripleIndexes \"spoc,posc,ospc,opsc,psoc,sopc,spoc,cpos,cosp,cops,cpso,csop\" ;\n"
308
//                                        + "                    config:sail.defaultQueryEvaluationMode \"STANDARD\"\n"
309
//                                        + "                ]\n"
310
//                                        + "            ]\n"
311
//                                        + "        ]\n"
312
//                                        + "    ].";
313

314
            String indexTypes = "spoc,posc,ospc,cspo,cpos,cosp";
315
            if (repoName.startsWith("meta") || repoName.startsWith("text")) {
316
                indexTypes = "spoc,posc,ospc";
317
            }
318

319
            String createRegularRepoQueryString =
320
                    "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\n" +
321
                    "@prefix rep: <http://www.openrdf.org/config/repository#>.\n" +
322
                    "@prefix sr: <http://www.openrdf.org/config/repository/sail#>.\n" +
323
                    "@prefix sail: <http://www.openrdf.org/config/sail#>.\n" +
324
                    "@prefix sail-luc: <http://www.openrdf.org/config/sail/lucene#>.\n" +
325
                    "@prefix lmdb: <http://rdf4j.org/config/sail/lmdb#>.\n" +
326
                    "@prefix sb: <http://www.openrdf.org/config/sail/base#>.\n" +
327
                    "\n" +
328
                    "[] a rep:Repository ;\n" +
329
                    "    rep:repositoryID \"" + repoName + "\" ;\n" +
330
                    "    rdfs:label \"" + repoName + " LMDB store\" ;\n" +
331
                    "    rep:repositoryImpl [\n" +
332
                    "        rep:repositoryType \"openrdf:SailRepository\" ;\n" +
333
                    "        sr:sailImpl [\n" +
334
                    "            sail:sailType \"rdf4j:LmdbStore\" ;\n" +
335
                    "            sail:iterationCacheSyncThreshold \"10000\";\n" +
336
                    "            lmdb:tripleIndexes \"" + indexTypes + "\" ;\n" +
337
                    "            sb:defaultQueryEvaluationMode \"STANDARD\"\n" +
338
                    "        ]\n"
339
                    + "    ].\n";
340

341
            // TODO Index npa:hasFilterLiteral predicate too (see https://groups.google.com/g/rdf4j-users/c/epF4Af1jXGU):
342
            String createTextRepoQueryString =
343
                    "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\n" +
344
                    "@prefix rep: <http://www.openrdf.org/config/repository#>.\n" +
345
                    "@prefix sr: <http://www.openrdf.org/config/repository/sail#>.\n" +
346
                    "@prefix sail: <http://www.openrdf.org/config/sail#>.\n" +
347
                    "@prefix sail-luc: <http://www.openrdf.org/config/sail/lucene#>.\n" +
348
                    "@prefix lmdb: <http://rdf4j.org/config/sail/lmdb#>.\n" +
349
                    "@prefix sb: <http://www.openrdf.org/config/sail/base#>.\n" +
350
                    "\n"
351
                    + "[] a rep:Repository ;\n" +
352
                    "    rep:repositoryID \"" + repoName + "\" ;\n" +
353
                    "    rdfs:label \"" + repoName + " store\" ;\n" +
354
                    "    rep:repositoryImpl [\n" +
355
                    "        rep:repositoryType \"openrdf:SailRepository\" ;\n" +
356
                    "        sr:sailImpl [\n" +
357
                    "            sail:sailType \"openrdf:LuceneSail\" ;\n" +
358
                    "            sail-luc:indexDir \"index/\" ;\n" +
359
                    "            sail-luc:transactional false ;\n" +
360
                    "            sail:delegate [\n" +
361
                    "              sail:sailType \"rdf4j:LmdbStore\" ;\n" +
362
                    "              sail:iterationCacheSyncThreshold \"10000\";\n" +
363
                    "              lmdb:tripleIndexes \"" + indexTypes + "\" ;\n" +
364
                    "              sb:defaultQueryEvaluationMode \"STANDARD\"\n" +
365
                    "            ]\n" +
366
                    "        ]\n" +
367
                    "    ].";
368

369
            String createRepoQueryString = createRegularRepoQueryString;
370
            if (repoName.startsWith("text")) {
371
                createRepoQueryString = createTextRepoQueryString;
372
            }
373

374
            HttpUriRequest createRepoRequest = RequestBuilder.put().setUri(endpointBase + "repositories/" + repoName).addHeader("Content-Type", "text/turtle").setEntity(new StringEntity(createRepoQueryString)).build();
375

376
            // Response is try-with-resources'd so the connection is released back to
377
            // the shared pool rather than leaked.
378
            try (CloseableHttpResponse response = httpclient.execute(createRepoRequest)) {
379
                int statusCode = response.getStatusLine().getStatusCode();
380
                if (statusCode == 409) {
381
                    //log.info("Already exists.");
382
                    getRepository(repoName).init();
383
                } else if (statusCode >= 200 && statusCode < 300) {
384
                    //log.info("Successfully created.");
385
                    initNewRepo(repoName);
386
                } else {
387
                    log.info("Status code: {}", response.getStatusLine().getStatusCode());
388
                    log.info(response.getStatusLine().getReasonPhrase());
389
                    String handledResponse = new BasicResponseHandler().handleResponse(response);
390
                    log.info("Response: {}", handledResponse);
391
                }
392
            }
393
        } catch (IOException ex) {
394
            log.info("Could not create repo.", ex);
395
        }
396
    }
397

398
    /**
399
     * Sends shutdown signal to all repositories.
400
     */
401
    @GeneratedFlagForDependentElements
402
    public void shutdownRepositories() {
403
        for (Repository repo : repositories.values()) {
404
            if (repo != null && repo.isInitialized()) {
405
                repo.shutDown();
406
            }
407
        }
408
    }
409

410
    /**
411
     * Returns admin repo connection.
412
     *
413
     * @return repository connection to admin repository
414
     */
415
    @GeneratedFlagForDependentElements
416
    public RepositoryConnection getAdminRepoConnection() {
417
        return get().getRepoConnection(ADMIN_REPO);
418
    }
419

420
    private Set<String> cachedRepositoryNames = Set.of();
9✔
421
    private boolean repoNamesCacheValid = false;
9✔
422
    private final ReadWriteLock repoNamesCacheLock = new ReentrantReadWriteLock();
15✔
423

424
    /**
425
     * Returns set of all repository names.
426
     *
427
     * @return Repository name set
428
     */
429
    public Set<String> getRepositoryNames() {
430
        // See if the repository names are cached:
431
        final var readLock = repoNamesCacheLock.readLock();
12✔
432
        try {
433
            readLock.lock();
6✔
434
            if (repoNamesCacheValid) {
9✔
435
                return cachedRepositoryNames;
15✔
436
            }
437
        } finally {
438
            readLock.unlock();
6✔
439
        }
440

441
        // Not cached, get from server:
442
        final var writeLock = repoNamesCacheLock.writeLock();
12✔
443
        try {
444
            writeLock.lock();
6✔
445
            // Check again if another thread has already updated the cache:
446
            if (repoNamesCacheValid) {
9!
447
                return cachedRepositoryNames;
×
448
            }
449
            Map<String, Boolean> repositoryNames = null;
6✔
450
            // Uses the shared this.httpclient; response try-with-resources releases
451
            // the pooled connection when done.
452
            try (CloseableHttpResponse resp = httpclient.execute(RequestBuilder.get()
24✔
453
                    .setUri(endpointBase + "/repositories")
9✔
454
                    .addHeader("Content-Type", "text/csv")
3✔
455
                    .build())) {
3✔
456
                BufferedReader reader = new BufferedReader(new InputStreamReader(resp.getEntity().getContent()));
30✔
457
                int code = resp.getStatusLine().getStatusCode();
12✔
458
                if (code < 200 || code >= 300) return null;
30!
459
                repositoryNames = new HashMap<>();
12✔
460
                int lineCount = 0;
6✔
461
                while (true) {
462
                    String line = reader.readLine();
9✔
463
                    if (line == null) break;
9✔
464
                    if (lineCount > 0) {
6✔
465
                        String repoName = line.split(",")[1];
18✔
466
                        repositoryNames.put(repoName, true);
18✔
467
                    }
468
                    lineCount = lineCount + 1;
12✔
469
                }
3✔
470
            } catch (IOException ex) {
15!
471
                log.info("Could not get repository names.", ex);
12✔
472
                return null;
12✔
473
            }
3✔
474
            cachedRepositoryNames = repositoryNames.keySet();
12✔
475
            repoNamesCacheValid = true;
9✔
476
            return cachedRepositoryNames;
15✔
477
        } finally {
478
            writeLock.unlock();
6✔
479
        }
480
    }
481

482
    /**
483
     * Invalidates the repository names cache. Call this method when a repository is created or deleted.
484
     */
485
    private void invalidateRepositoryNamesCache() {
486
        final var writeLock = repoNamesCacheLock.writeLock();
×
487
        try {
488
            writeLock.lock();
×
489
            repoNamesCacheValid = false;
×
490
        } finally {
491
            writeLock.unlock();
×
492
        }
493
    }
×
494

495
    @GeneratedFlagForDependentElements
496
    private void initNewRepo(String repoName) {
497
        String repoInitId = new Random().nextLong() + "";
498
        getRepository(repoName).init();
499
        if (!repoName.equals("empty")) {
500
            RepositoryConnection conn = getRepoConnection(repoName);
501
            try (conn) {
502
                // Full isolation, just in case.
503
                conn.begin(IsolationLevels.SERIALIZABLE);
504
                conn.add(NPA.THIS_REPO, NPA.HAS_REPO_INIT_ID, vf.createLiteral(repoInitId), NPA.GRAPH);
505
                if (tracksNanopubCountAndChecksum(repoName)) {
506
                    conn.add(NPA.THIS_REPO, NPA.HAS_NANOPUB_COUNT, vf.createLiteral(0L), NPA.GRAPH);
507
                    conn.add(NPA.THIS_REPO, NPA.HAS_NANOPUB_CHECKSUM, vf.createLiteral(NanopubUtils.INIT_CHECKSUM), NPA.GRAPH);
508
                }
509
                if (repoName.startsWith("pubkey_") || repoName.startsWith("type_")) {
510
                    String h = repoName.replaceFirst("^[^_]+_", "");
511
                    conn.add(NPA.THIS_REPO, NPA.HAS_COVERAGE_ITEM, Utils.getObjectForHash(h), NPA.GRAPH);
512
                    conn.add(NPA.THIS_REPO, NPA.HAS_COVERAGE_HASH, vf.createLiteral(h), NPA.GRAPH);
513
                    conn.add(NPA.THIS_REPO, NPA.HAS_COVERAGE_FILTER, vf.createLiteral("_" + repoName), NPA.GRAPH);
514
                }
515
                conn.commit();
516
            }
517
            // Refresh repository names cache
518
            invalidateRepositoryNamesCache();
519
        }
520
    }
521

522
    /**
523
     * Whether the given repo participates in the cumulative nanopub-count / XOR-checksum
524
     * tracking. Repos that don't produce their own content-addressed population
525
     * skip the {@code npa:hasNanopubCount} and {@code npa:hasNanopubChecksum}
526
     * initial triples — leaving them at {@code 0} and the empty-XOR placeholder
527
     * forever would just be misleading. Currently excluded:
528
     * <ul>
529
     *   <li>{@code admin} — holds metadata only.</li>
530
     *   <li>{@code last30d} — content expires on a periodic cleanup.</li>
531
     *   <li>{@code trust} — holds derived trust state, mirrored from the registry.</li>
532
     *   <li>{@code spaces} — holds space-relevant raw nanopubs (a filtered projection
533
     *       of {@code full}) plus extraction triples; its XOR checksum over loaded
534
     *       trusty URIs would diverge from {@code full}'s without adding value.</li>
535
     * </ul>
536
     */
537
    private static boolean tracksNanopubCountAndChecksum(String repoName) {
538
        return !repoName.equals(ADMIN_REPO)
×
539
                && !repoName.equals("last30d")
×
540
                && !repoName.equals("trust")
×
541
                && !repoName.equals("spaces");
×
542
    }
543

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

© 2026 Coveralls, Inc