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

knowledgepixels / nanopub-query / 26054523970

18 May 2026 07:07PM UTC coverage: 58.571% (-0.2%) from 58.799%
26054523970

Pull #101

github

web-flow
Merge a3ff19740 into 2428aa94a
Pull Request #101: Surface and contain silent load-time failures

498 of 932 branches covered (53.43%)

Branch coverage included in aggregate %.

1330 of 2189 relevant lines covered (60.76%)

9.26 hits per line

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

27.24
src/main/java/com/knowledgepixels/query/JellyNanopubLoader.java
1
package com.knowledgepixels.query;
2

3
import java.io.IOException;
4
import java.util.concurrent.atomic.AtomicLong;
5

6
import org.apache.http.client.methods.CloseableHttpResponse;
7
import org.apache.http.client.methods.HttpGet;
8
import org.apache.http.client.methods.HttpHead;
9
import org.apache.http.impl.client.CloseableHttpClient;
10
import org.apache.http.impl.client.HttpClientBuilder;
11
import org.apache.http.util.EntityUtils;
12
import org.nanopub.NanopubUtils;
13
import org.nanopub.jelly.NanopubStream;
14
import org.slf4j.Logger;
15
import org.slf4j.LoggerFactory;
16

17
/**
18
 * Loads nanopubs from the attached Nanopub Registry via a restartable Jelly stream.
19
 */
20
public class JellyNanopubLoader {
×
21
    static final String registryUrl;
22
    private static long lastCommittedCounter = -1;
6✔
23
    private static Long lastKnownSetupId = null;
6✔
24
    // Latest registry metadata fields, updated on each metadata fetch and forwarded to clients
25
    static volatile String lastCoverageTypes = null;
6✔
26
    static volatile String lastCoverageAgents = null;
6✔
27
    static volatile String lastTestInstance = null;
6✔
28
    static volatile String lastNanopubCount = null;
6✔
29
    private static final CloseableHttpClient metadataClient;
30
    private static final CloseableHttpClient jellyStreamClient;
31

32
    private static final int MAX_RETRIES_METADATA = 10;
33
    private static final int RETRY_DELAY_METADATA = 3000;
34
    private static final int RETRY_DELAY_JELLY = 5000;
35

36
    /**
37
     * Circuit-breaker state. Counts consecutive {@code loadUpdates} invocations in
38
     * which the catch block fired; resets to zero on the next successful batch.
39
     * Scheduled on the single-threaded executor in {@link MainVerticle}, so a plain
40
     * {@code int} is enough — no concurrency.
41
     *
42
     * <p>When the counter reaches {@link #BREAKER_THRESHOLD}, the next invocation
43
     * sleeps {@link #BREAKER_PAUSE_MS} before proceeding. Lets the saturated RDF4J
44
     * drain instead of being hammered every {@link #UPDATES_POLL_INTERVAL} ms.
45
     *
46
     * <p>Depends on {@link com.knowledgepixels.query.TripleStore}'s socket timeouts
47
     * (change 1 of the fix plan) to turn parked commits into propagating exceptions;
48
     * without them, {@code loadBatch} can park forever, the catch never fires, and
49
     * this counter stays at zero.
50
     */
51
    static volatile int consecutiveBatchFailures = 0;
6✔
52

53
    static final int BREAKER_THRESHOLD = 3;
54
    static final long BREAKER_PAUSE_MS = 30_000L;
55

56
    /**
57
     * Epoch-millis of the last successful {@code loadUpdates} invocation (whether it
58
     * actually loaded a batch or was a "caught up, nothing to do" tick). Read by
59
     * {@link MetricsCollector} to expose {@code registry.loader.last_successful_batch_age_seconds}
60
     * so an operator can tell at a glance from Grafana whether an instance is still
61
     * making progress. Updated on every non-exceptional return from loadUpdates,
62
     * including idle returns — an instance that is caught up and correctly polling
63
     * still counts as alive.
64
     */
65
    static volatile long lastSuccessfulBatchAtMs = 0L;
6✔
66

67
    /**
68
     * Heartbeat counter for loadUpdates invocations. A summary log line is emitted
69
     * every {@link #HEARTBEAT_INTERVAL_INVOCATIONS} invocations so a truncated or
70
     * sampled log still shows the loader's state evolving. At the default 2 s poll
71
     * interval, 30 invocations ≈ 1 line per minute.
72
     */
73
    private static long loadUpdatesInvocations = 0L;
6✔
74
    private static final long HEARTBEAT_INTERVAL_INVOCATIONS = 30;
75

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

78
    /**
79
     * Registry metadata returned by a HEAD request.
80
     */
81
    record RegistryMetadata(long loadCounter, Long setupId, String coverageTypes,
72✔
82
                            String coverageAgents, String testInstance, String nanopubCount,
83
                            String trustStateHash) {}
84

85
    /**
86
     * The interval in milliseconds at which the updates loader should poll for new nanopubs.
87
     */
88
    public static final int UPDATES_POLL_INTERVAL = 2000;
89

90
    enum LoadingType {
×
91
        INITIAL,
×
92
        UPDATE,
×
93
    }
94

95
    static {
96
        // Initialize registryUrl
97
        var url = Utils.getEnvString(
12✔
98
                "REGISTRY_FIXED_URL", "https://registry.knowledgepixels.com/"
99
        );
100
        if (!url.endsWith("/")) url += "/";
12!
101
        registryUrl = url;
6✔
102

103
        metadataClient = HttpClientBuilder.create().setDefaultRequestConfig(Utils.getHttpRequestConfig()).build();
15✔
104
        jellyStreamClient = NanopubUtils.getHttpClient();
6✔
105
    }
3✔
106

107
    /**
108
     * Start or continue (after restart) the initial loading procedure. This simply loads all
109
     * nanopubs from the attached Registry.
110
     *
111
     * @param afterCounter which counter to start from (-1 for the beginning)
112
     */
113
    public static void loadInitial(long afterCounter) {
114
        RegistryMetadata metadata = fetchRegistryMetadata();
6✔
115
        updateForwardingMetadata(metadata);
6✔
116
        TrustStateLoader.maybeUpdate(metadata.trustStateHash());
9✔
117
        long targetCounter = metadata.loadCounter();
9✔
118
        log.info("Fetched Registry load counter: {}", targetCounter);
15✔
119
        // Store setupId on initial load
120
        if (metadata.setupId() != null && lastKnownSetupId == null) {
9!
121
            lastKnownSetupId = metadata.setupId();
×
122
            StatusController.get().setRegistrySetupId(metadata.setupId());
×
123
        }
124
        lastCommittedCounter = afterCounter;
6✔
125
        while (lastCommittedCounter < targetCounter) {
12!
126
            // Same circuit-breaker logic as loadUpdates: after BREAKER_THRESHOLD
127
            // consecutive failed batches, pause before retrying so a saturated RDF4J
128
            // (e.g. during a restart storm) can drain instead of being hammered on the
129
            // 5-second RETRY_DELAY_JELLY cadence.
130
            if (consecutiveBatchFailures >= BREAKER_THRESHOLD) {
×
131
                log.warn("Circuit breaker active during initial load after {} consecutive batch failures; pausing {} ms before next attempt",
×
132
                        consecutiveBatchFailures, BREAKER_PAUSE_MS);
×
133
                try {
134
                    Thread.sleep(BREAKER_PAUSE_MS);
×
135
                } catch (InterruptedException e) {
×
136
                    Thread.currentThread().interrupt();
×
137
                    throw new RuntimeException("Interrupted while waiting for circuit breaker.");
×
138
                }
×
139
            }
140
            try {
141
                loadBatch(lastCommittedCounter, LoadingType.INITIAL);
×
142
                consecutiveBatchFailures = 0;
×
143
                log.info("Initial load: loaded batch up to counter {}", lastCommittedCounter);
×
144
            } catch (Exception e) {
×
145
                consecutiveBatchFailures++;
×
146
                log.info("Failed to load batch starting from counter {} (consecutive failures: {})",
×
147
                        lastCommittedCounter, consecutiveBatchFailures);
×
148
                log.info("Failure reason: ", e);
×
149
                try {
150
                    Thread.sleep(RETRY_DELAY_JELLY);
×
151
                } catch (InterruptedException e2) {
×
152
                    throw new RuntimeException("Interrupted while waiting to retry loading batch.");
×
153
                }
×
154
            }
×
155
        }
156
        log.info("Initial load complete.");
9✔
157
    }
3✔
158

159
    /**
160
     * Check if the Registry has any new nanopubs. If it does, load them.
161
     * This method should be called periodically, and you should wait for it to finish before
162
     * calling it again.
163
     */
164
    public static void loadUpdates() {
165
        // Circuit breaker: after BREAKER_THRESHOLD consecutive failed batches, pause
166
        // before the next attempt so a saturated RDF4J can drain. Check happens before
167
        // any RDF4J-touching work so the sleep isn't itself under the broken regime.
168
        if (consecutiveBatchFailures >= BREAKER_THRESHOLD) {
×
169
            log.warn("Circuit breaker active after {} consecutive batch failures; pausing {} ms before next attempt",
×
170
                    consecutiveBatchFailures, BREAKER_PAUSE_MS);
×
171
            try {
172
                Thread.sleep(BREAKER_PAUSE_MS);
×
173
            } catch (InterruptedException e) {
×
174
                // Preserve interruption semantics so a graceful shutdown (e.g. via
175
                // MainVerticle's shutdown hook) isn't blocked by the pause.
176
                Thread.currentThread().interrupt();
×
177
                return;
×
178
            }
×
179
        }
180
        try {
181
            final var status = StatusController.get().getState();
×
182
            lastCommittedCounter = status.loadCounter;
×
183
            RegistryMetadata metadata = fetchRegistryMetadata();
×
184
            updateForwardingMetadata(metadata);
×
185
            TrustStateLoader.maybeUpdate(metadata.trustStateHash());
×
186
            long targetCounter = metadata.loadCounter();
×
187
            Long currentSetupId = metadata.setupId();
×
188

189
            // Detect reset via setupId change
190
            if (lastKnownSetupId != null && currentSetupId != null
×
191
                    && !lastKnownSetupId.equals(currentSetupId)) {
×
192
                log.warn("Registry reset detected: setupId {} -> {}", lastKnownSetupId, currentSetupId);
×
193
                performResync(currentSetupId);
×
194
                return;
×
195
            }
196
            // Detect reset via counter decrease (also covers first run after upgrade
197
            // where no setupId was persisted yet but the registry has already been reset)
198
            if (lastCommittedCounter > 0 && targetCounter >= 0
×
199
                    && targetCounter < lastCommittedCounter) {
200
                log.warn("Registry counter decreased {} -> {}, triggering resync",
×
201
                        lastCommittedCounter, targetCounter);
×
202
                performResync(currentSetupId);
×
203
                return;
×
204
            }
205

206
            // Update lastKnownSetupId on first successful poll
207
            if (currentSetupId != null && lastKnownSetupId == null) {
×
208
                if (lastCommittedCounter > 0) {
×
209
                    // Upgrade from a version without setupId tracking. The DB has data but
210
                    // we can't verify it matches the current registry. Force a resync.
211
                    log.warn("No stored setupId but DB has data (counter: {}). "
×
212
                            + "Forcing resync to ensure data consistency.", lastCommittedCounter);
×
213
                    performResync(currentSetupId);
×
214
                    return;
×
215
                }
216
                lastKnownSetupId = currentSetupId;
×
217
                StatusController.get().setRegistrySetupId(currentSetupId);
×
218
            }
219

220
            if (lastCommittedCounter >= targetCounter) {
×
221
                // Nothing to do. Keep state at READY (setReady is idempotent) and
222
                // skip the redundant setLoadingUpdates → setReady admin-repo write
223
                // that the old flow did on every idle poll. Also reset the breaker
224
                // counter — a successful "nothing to do" is still a successful tick
225
                // and should clear stale failure state from earlier transient errors.
226
                StatusController.get().setReady();
×
227
                consecutiveBatchFailures = 0;
×
228
                lastSuccessfulBatchAtMs = System.currentTimeMillis();
×
229
                maybeLogHeartbeat(targetCounter, true);
×
230
                return;
×
231
            }
232
            StatusController.get().setLoadingUpdates(status.loadCounter);
×
233
            loadBatch(lastCommittedCounter, LoadingType.UPDATE);
×
234
            // Batch completed without an exception — reset the breaker counter.
235
            consecutiveBatchFailures = 0;
×
236
            lastSuccessfulBatchAtMs = System.currentTimeMillis();
×
237
            maybeLogHeartbeat(targetCounter, false);
×
238
            log.info("Loaded {} update(s). Counter: {}, target was: {}",
×
239
                    lastCommittedCounter - status.loadCounter, lastCommittedCounter, targetCounter);
×
240
            if (lastCommittedCounter < targetCounter) {
×
241
                log.info("Warning: expected to load nanopubs up to (inclusive) counter " +
×
242
                        targetCounter + " based on the counter reported in Registry's headers, " +
243
                        "but loaded only up to {}.", lastCommittedCounter);
×
244
            }
245
        } catch (Exception e) {
×
246
            consecutiveBatchFailures++;
×
247
            log.warn("Failed to load updates. Current counter: {} (consecutive failures: {})",
×
248
                    lastCommittedCounter, consecutiveBatchFailures, e);
×
249
        } finally {
250
            try {
251
                StatusController.get().setReady();
×
252
            } catch (Exception e) {
×
253
                log.info("Update loader: failed to set status to READY.");
×
254
                log.info("Failure Reason: ", e);
×
255
            }
×
256
        }
257
    }
×
258

259
    /**
260
     * Emit a heartbeat summary log line roughly every
261
     * {@link #HEARTBEAT_INTERVAL_INVOCATIONS} invocations of {@link #loadUpdates}.
262
     * Lets an operator reconstruct loader progress from a sparse or sampled log
263
     * export, independent of Prometheus retention.
264
     */
265
    private static void maybeLogHeartbeat(long targetCounter, boolean idle) {
266
        loadUpdatesInvocations++;
×
267
        if (loadUpdatesInvocations % HEARTBEAT_INTERVAL_INVOCATIONS != 0) return;
×
268
        log.info("Loader heartbeat: counter={} target={} idle={} consecutiveBatchFailures={} breakerActive={}",
×
269
                lastCommittedCounter, targetCounter, idle, consecutiveBatchFailures,
×
270
                consecutiveBatchFailures >= BREAKER_THRESHOLD);
×
271
    }
×
272

273
    /**
274
     * Re-stream all nanopubs from the registry after a reset is detected.
275
     * Existing nanopubs are skipped by NanopubLoader's per-repo dedup.
276
     *
277
     * @param newSetupId the new setup ID from the registry, or null if unknown
278
     */
279
    private static void performResync(Long newSetupId) {
280
        log.warn("Starting resync with registry. New setupId: {}", newSetupId);
×
281
        StatusController.get().setResetting();
×
282
        lastKnownSetupId = newSetupId;
×
283
        if (newSetupId != null) {
×
284
            StatusController.get().setRegistrySetupId(newSetupId);
×
285
        }
286
        StatusController.get().setLoadingInitial(-1);
×
287
        loadInitial(-1);
×
288
        StatusController.get().setReady();
×
289
        log.warn("Resync complete. Counter: {}", lastCommittedCounter);
×
290
    }
×
291

292
    /**
293
     * Load a batch of nanopubs from the Jelly stream.
294
     * <p>
295
     * The method requests the list of all nanopubs from the Registry and reads it for as long
296
     * as it can. If the stream is interrupted, the method will throw an exception, and you
297
     * can resume loading from the last known counter.
298
     *
299
     * @param afterCounter the last known nanopub counter to have been committed in the DB
300
     * @param type         the type of loading operation (initial or update)
301
     */
302
    static void loadBatch(long afterCounter, LoadingType type) {
303
        CloseableHttpResponse response;
304
        try {
305
            var request = new HttpGet(makeStreamFetchUrl(afterCounter));
×
306
            response = jellyStreamClient.execute(request);
×
307
        } catch (IOException e) {
×
308
            throw new RuntimeException("Failed to fetch Jelly stream from the Registry (I/O error).", e);
×
309
        }
×
310

311
        int httpStatus = response.getStatusLine().getStatusCode();
×
312
        if (httpStatus < 200 || httpStatus >= 300) {
×
313
            EntityUtils.consumeQuietly(response.getEntity());
×
314
            throw new RuntimeException("Jelly stream HTTP status is not 2xx: " + httpStatus + ".");
×
315
        }
316

317
        try (
318
                var is = response.getEntity().getContent();
×
319
                var npStream = NanopubStream.fromByteStream(is).getAsNanopubs()
×
320
        ) {
321
            AtomicLong checkpointTime = new AtomicLong(System.currentTimeMillis());
×
322
            AtomicLong checkpointCounter = new AtomicLong(lastCommittedCounter);
×
323
            AtomicLong lastSavedCounter = new AtomicLong(lastCommittedCounter);
×
324
            AtomicLong loaded = new AtomicLong(0L);
×
325

326
            npStream.forEach(m -> {
×
327
                if (!m.isSuccess()) throw new RuntimeException("Failed to load " +
×
328
                        "nanopub from Jelly stream. Last known counter: " + lastCommittedCounter,
329
                        m.getException()
×
330
                );
331
                if (m.getCounter() < lastCommittedCounter) {
×
332
                    throw new RuntimeException("Received a nanopub with a counter lower than " +
×
333
                            "the last known counter. Last known counter: " + lastCommittedCounter +
334
                            ", received counter: " + m.getCounter());
×
335
                }
336
                NanopubLoader.load(m.getNanopub(), m.getCounter());
×
337
                // Bump the in-memory counter BEFORE persisting it. The previous order
338
                // wrote the *previous* nanopub's counter to the DB at each checkpoint,
339
                // so a crash-restart silently re-processed one extra nanopub and the
340
                // contract "saved counter == last fully loaded nanopub" was violated.
341
                lastCommittedCounter = m.getCounter();
×
342
                if (m.getCounter() % 10 == 0) {
×
343
                    // Save the committed counter only every 10 nanopubs to reduce DB load
344
                    saveCommittedCounter(type);
×
345
                    lastSavedCounter.set(m.getCounter());
×
346
                }
347
                loaded.getAndIncrement();
×
348

349
                if (loaded.get() % 50 == 0) {
×
350
                    long currTime = System.currentTimeMillis();
×
351
                    double speed = 50 / ((currTime - checkpointTime.get()) / 1000.0);
×
352
                    log.info("Loading speed: " + String.format("%.2f", speed) +
×
353
                            " np/s. Counter: " + lastCommittedCounter);
354
                    checkpointTime.set(currTime);
×
355
                    checkpointCounter.set(lastCommittedCounter);
×
356
                }
357
            });
×
358
            // Make sure to save the last committed counter at the end of the batch
359
            if (lastCommittedCounter >= lastSavedCounter.get()) {
×
360
                saveCommittedCounter(type);
×
361
            }
362
        } catch (IOException e) {
×
363
            throw new RuntimeException("I/O error while reading the response Jelly stream.", e);
×
364
        } finally {
365
            try {
366
                response.close();
×
367
            } catch (IOException e) {
×
368
                log.info("Failed to close the Jelly stream response.");
×
369
            }
×
370
        }
371
    }
×
372

373
    /**
374
     * Save the last committed counter to the DB. Do this every N nanopubs to reduce DB load.
375
     * Remember to call this method at the end of the batch as well.
376
     *
377
     * @param type the type of loading operation (initial or update)
378
     */
379
    private static void saveCommittedCounter(LoadingType type) {
380
        try {
381
            if (type == LoadingType.INITIAL) {
×
382
                StatusController.get().setLoadingInitial(lastCommittedCounter);
×
383
            } else {
384
                StatusController.get().setLoadingUpdates(lastCommittedCounter);
×
385
            }
386
        } catch (Exception e) {
×
387
            throw new RuntimeException("Could not update the nanopub counter in DB", e);
×
388
        }
×
389
    }
×
390

391
    /**
392
     * Set the last known setup ID. Called from MainVerticle on startup to restore persisted state.
393
     *
394
     * @param setupId the setup ID to set, or null if not known
395
     */
396
    static void setLastKnownSetupId(Long setupId) {
397
        lastKnownSetupId = setupId;
×
398
    }
×
399

400
    /**
401
     * Update the cached metadata fields used for forwarding to clients.
402
     */
403
    private static void updateForwardingMetadata(RegistryMetadata metadata) {
404
        lastCoverageTypes = metadata.coverageTypes();
9✔
405
        lastCoverageAgents = metadata.coverageAgents();
9✔
406
        lastTestInstance = metadata.testInstance();
9✔
407
        lastNanopubCount = metadata.nanopubCount();
9✔
408
    }
3✔
409

410
    /**
411
     * Run a HEAD request to the Registry to fetch its current metadata (load counter and setup ID).
412
     *
413
     * @return the registry metadata
414
     */
415
    static RegistryMetadata fetchRegistryMetadata() {
416
        int tries = 0;
6✔
417
        RegistryMetadata metadata = null;
6✔
418
        while (metadata == null && tries < MAX_RETRIES_METADATA) {
15!
419
            try {
420
                metadata = fetchRegistryMetadataInner();
4✔
421
            } catch (Exception e) {
1✔
422
                tries++;
1✔
423
                log.info("Failed to fetch registry metadata, try " + tries +
5✔
424
                        ". Retrying in {}ms...", RETRY_DELAY_METADATA);
1✔
425
                log.info("Failure Reason: ", e);
4✔
426
                try {
427
                    Thread.sleep(RETRY_DELAY_METADATA);
2✔
428
                } catch (InterruptedException e2) {
×
429
                    throw new RuntimeException(
×
430
                            "Interrupted while waiting to retry fetching registry metadata.");
431
                }
1✔
432
            }
3✔
433
        }
434
        if (metadata == null) {
6!
435
            throw new RuntimeException("Failed to fetch registry metadata after " +
5✔
436
                    MAX_RETRIES_METADATA + " retries.");
437
        }
438
        return metadata;
4✔
439
    }
440

441
    /**
442
     * Inner logic for fetching the registry metadata via HEAD request.
443
     *
444
     * @return the registry metadata (load counter and setup ID)
445
     * @throws IOException if the HTTP request fails
446
     */
447
    private static RegistryMetadata fetchRegistryMetadataInner() throws IOException {
448
        var request = new HttpHead(registryUrl);
15✔
449
        try (var response = metadataClient.execute(request)) {
8✔
450
            int status = response.getStatusLine().getStatusCode();
8✔
451
            EntityUtils.consumeQuietly(response.getEntity());
6✔
452
            if (status < 200 || status >= 300) {
12!
453
                throw new RuntimeException("Registry metadata HTTP status is not 2xx: " +
×
454
                        status + ".");
455
            }
456

457
            // Check if the registry is ready
458
            var hStatus = response.getHeaders("Nanopub-Registry-Status");
8✔
459
            if (hStatus.length == 0) {
6!
460
                throw new RuntimeException("Registry did not return a Nanopub-Registry-Status header.");
×
461
            }
462
            if (!"ready".equals(hStatus[0].getValue()) && !"updating".equals(hStatus[0].getValue())) {
14!
463
                throw new RuntimeException("Registry is not in ready state.");
×
464
            }
465

466
            // Get the load counter
467
            var hCounter = response.getHeaders("Nanopub-Registry-Load-Counter");
8✔
468
            if (hCounter.length == 0) {
6!
469
                throw new RuntimeException("Registry did not return a Nanopub-Registry-Load-Counter header.");
×
470
            }
471
            long loadCounter = Long.parseLong(hCounter[0].getValue());
12✔
472

473
            // Get the setup ID (optional — older registries may not have it)
474
            Long setupId = null;
4✔
475
            var hSetupId = response.getHeaders("Nanopub-Registry-Setup-Id");
8✔
476
            if (hSetupId.length > 0) {
6!
477
                try {
478
                    setupId = Long.parseLong(hSetupId[0].getValue());
14✔
479
                } catch (NumberFormatException e) {
×
480
                    log.info("Could not parse Nanopub-Registry-Setup-Id header: {}", hSetupId[0].getValue());
×
481
                }
2✔
482
            }
483

484
            // Read metadata headers for forwarding to clients
485
            String coverageTypes = getHeaderValue(response, "Nanopub-Registry-Coverage-Types");
8✔
486
            String coverageAgents = getHeaderValue(response, "Nanopub-Registry-Coverage-Agents");
8✔
487
            String testInstance = getHeaderValue(response, "Nanopub-Registry-Test-Instance");
8✔
488
            String nanopubCount = getHeaderValue(response, "Nanopub-Registry-Nanopub-Count");
8✔
489
            // Optional — older registries (without trust calculation) won't set this header.
490
            String trustStateHash = getHeaderValue(response, "Nanopub-Registry-Trust-State-Hash");
8✔
491

492
            return new RegistryMetadata(loadCounter, setupId, coverageTypes, coverageAgents,
26✔
493
                    testInstance, nanopubCount, trustStateHash);
494
        }
495
    }
496

497
    private static String getHeaderValue(CloseableHttpResponse response, String name) {
498
        var headers = response.getHeaders(name);
8✔
499
        return headers.length > 0 ? headers[0].getValue() : null;
18!
500
    }
501

502
    /**
503
     * Construct the URL for fetching the Jelly stream.
504
     *
505
     * @param afterCounter the last known counter to have been committed in the DB
506
     * @return the URL for fetching the Jelly stream
507
     */
508
    private static String makeStreamFetchUrl(long afterCounter) {
509
        return registryUrl + "nanopubs.jelly?afterCounter=" + afterCounter;
×
510
    }
511
}
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