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

knowledgepixels / nanopub-query / 24127353986

08 Apr 2026 09:07AM UTC coverage: 66.922% (-0.08%) from 66.999%
24127353986

push

github

tkuhn
fix: prevent event loop blocking from network I/O and lock contention

- Cache nanopub lookups in GrlcSpec to avoid blocking GetNanopub.get()
  calls on the Vert.x event loop for every API request
- Wrap /, /pubkeys, /types handlers in executeBlocking to prevent
  getRepositoryNames() HTTP calls from blocking the event loop
- Make StatusController.getState() lock-free with volatile fields
- Increase HTTP fetch timeouts from 1s to 10s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

227 of 378 branches covered (60.05%)

Branch coverage included in aggregate %.

645 of 925 relevant lines covered (69.73%)

10.15 hits per line

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

0.0
src/main/java/com/knowledgepixels/query/MainVerticle.java
1
package com.knowledgepixels.query;
2

3
import com.github.jsonldjava.shaded.com.google.common.base.Charsets;
4
import com.knowledgepixels.query.GrlcSpec.InvalidGrlcSpecException;
5
import io.micrometer.prometheus.PrometheusMeterRegistry;
6
import io.vertx.core.AbstractVerticle;
7
import io.vertx.core.Future;
8
import io.vertx.core.Promise;
9
import io.vertx.core.buffer.Buffer;
10
import io.vertx.core.http.*;
11
import io.vertx.ext.web.Router;
12
import io.vertx.ext.web.RoutingContext;
13
import io.vertx.ext.web.handler.CorsHandler;
14
import io.vertx.ext.web.handler.StaticHandler;
15
import io.vertx.ext.web.proxy.handler.ProxyHandler;
16
import io.vertx.httpproxy.*;
17
import io.vertx.micrometer.PrometheusScrapingHandler;
18
import io.vertx.micrometer.backends.BackendRegistries;
19
import org.eclipse.rdf4j.model.Value;
20
import org.slf4j.Logger;
21
import org.slf4j.LoggerFactory;
22

23
import java.io.InputStream;
24
import java.net.URLEncoder;
25
import java.util.ArrayList;
26
import java.util.Collections;
27
import java.util.List;
28
import java.util.Scanner;
29
import java.util.concurrent.Executors;
30
import java.util.concurrent.TimeUnit;
31

32
/**
33
 * Main verticle that coordinates the incoming HTTP requests.
34
 */
35
@GeneratedFlagForDependentElements
36
public class MainVerticle extends AbstractVerticle {
37

38
    private static String css = null;
39

40
    private static final Logger log = LoggerFactory.getLogger(MainVerticle.class);
41

42
    /**
43
     * Start the main verticle.
44
     *
45
     * @param startPromise the promise to complete when the verticle is started
46
     * @throws Exception if an error occurs during startup
47
     */
48
    @Override
49
    public void start(Promise<Void> startPromise) throws Exception {
50
        HttpClient httpClient = vertx.createHttpClient(
51
                new HttpClientOptions()
52
                        .setConnectTimeout(Utils.getEnvInt("NANOPUB_QUERY_VERTX_CONNECT_TIMEOUT", 1000))
53
                        .setIdleTimeoutUnit(TimeUnit.SECONDS)
54
                        .setIdleTimeout(Utils.getEnvInt("NANOPUB_QUERY_VERTX_IDLE_TIMEOUT", 60))
55
                        .setReadIdleTimeout(Utils.getEnvInt("NANOPUB_QUERY_VERTX_IDLE_TIMEOUT", 60))
56
                        .setWriteIdleTimeout(Utils.getEnvInt("NANOPUB_QUERY_VERTX_IDLE_TIMEOUT", 60)),
57
                new PoolOptions().setHttp1MaxSize(200).setHttp2MaxSize(200)
58
        );
59

60
        HttpServer proxyServer = vertx.createHttpServer(
61
                new HttpServerOptions().setMaxInitialLineLength(65536)
62
        );
63
        Router proxyRouter = Router.router(vertx);
64
        proxyRouter.route().handler(CorsHandler.create().addRelativeOrigin(".*"));
65

66
        // Metrics
67
        final var metricsHttpServer = vertx.createHttpServer();
68
        final var metricsRouter = Router.router(vertx);
69
        metricsHttpServer.requestHandler(metricsRouter).listen(9394);
70

71
        final var metricsRegistry = (PrometheusMeterRegistry) BackendRegistries.getDefaultNow();
72
        final var collector = new MetricsCollector(metricsRegistry);
73
        metricsRouter.route("/metrics").handler(PrometheusScrapingHandler.create(metricsRegistry));
74
        // ----------
75
        // This part is only used if the redirection is not done through Nginx.
76
        // See nginx.conf and this bug report: https://github.com/eclipse-rdf4j/rdf4j/discussions/5120
77
        HttpProxy rdf4jProxy = HttpProxy.reverseProxy(httpClient);
78
        String proxy = Utils.getEnvString("RDF4J_PROXY_HOST", "rdf4j");
79
        int proxyPort = Utils.getEnvInt("RDF4J_PROXY_PORT", 8080);
80
        rdf4jProxy.origin(proxyPort, proxy);
81

82
        rdf4jProxy.addInterceptor(new ProxyInterceptor() {
×
83

84
            @Override
85
            @GeneratedFlagForDependentElements
86
            public Future<ProxyResponse> handleProxyRequest(ProxyContext context) {
87
                ProxyRequest request = context.request();
88
                request.setURI(request.getURI().replaceAll("/", "_").replaceFirst("^_repo_", "/rdf4j-server/repositories/"));
89
                // For later to try to get HTML tables out:
90
//                                if (request.headers().get("Accept") == null) {
91
//                                        request.putHeader("Accept", "text/html");
92
//                                }
93
//                                request.putHeader("Accept", "application/json");
94
                return ProxyInterceptor.super.handleProxyRequest(context);
95
            }
96

97
            @Override
98
            @GeneratedFlagForDependentElements
99
            public Future<Void> handleProxyResponse(ProxyContext context) {
100
                ProxyResponse resp = context.response();
101
                resp.putHeader("Access-Control-Allow-Origin", "*");
102
                resp.putHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
103
                // For later to try to get HTML tables out:
104
//                                String acceptHeader = context.request().headers().get("Accept");
105
//                                if (acceptHeader != null && acceptHeader.contains("text/html")) {
106
//                                        resp.putHeader("Content-Type", "text/html");
107
//                                        resp.setBody(Body.body(Buffer.buffer("<html><body><strong>test</strong></body></html>")));
108
//                                }
109
                return ProxyInterceptor.super.handleProxyResponse(context);
110
            }
111

112
        });
113
        // ----------
114

115
        proxyRouter.route(HttpMethod.GET, "/repo").handler(req -> handleRedirect(req, "/repo"));
116
        proxyRouter.route(HttpMethod.GET, "/repo/*").handler(ProxyHandler.create(rdf4jProxy));
117
        proxyRouter.route(HttpMethod.POST, "/repo/*").handler(ProxyHandler.create(rdf4jProxy));
118
        proxyRouter.route(HttpMethod.HEAD, "/repo/*").handler(ProxyHandler.create(rdf4jProxy));
119
        proxyRouter.route(HttpMethod.OPTIONS, "/repo/*").handler(ProxyHandler.create(rdf4jProxy));
120
        proxyRouter.route(HttpMethod.GET, "/tools/*").handler(req -> {
121
            final String yasguiPattern = "^/tools/([a-zA-Z0-9-_]+)(/([a-zA-Z0-9-_]+))?/yasgui\\.html$";
122
            if (req.normalizedPath().matches(yasguiPattern)) {
123
                String repo = req.normalizedPath().replaceFirst(yasguiPattern, "$1$2");
124
                req.response()
125
                        .putHeader("content-type", "text/html")
126
                        .end("<!DOCTYPE html>\n"
127
                             + "<html lang=\"en\">\n"
128
                             + "<head>\n"
129
                             + "<meta charset=\"utf-8\">\n"
130
                             + "<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n"
131
                             + "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n"
132
                             + "<title>Nanopub Query SPARQL Editor for repository: " + repo + "</title>\n"
133
                             + "<link rel=\"stylesheet\" href=\"/style.css\">\n"
134
                             + "<link href='https://cdn.jsdelivr.net/yasgui/2.6.1/yasgui.min.css' rel='stylesheet' type='text/css'/>\n"
135
                             + "<style>.yasgui .endpointText {display:none !important;}</style>\n"
136
                             + "<script type=\"text/javascript\">localStorage.clear();</script>\n"
137
                             + "</head>\n"
138
                             + "<body>\n"
139
                             + "<h3>Nanopub Query SPARQL Editor for repository: " + repo + "</h3>\n"
140
                             + "<div id='yasgui'></div>\n"
141
                             + "<script src='https://cdn.jsdelivr.net/yasgui/2.6.1/yasgui.min.js'></script>\n"
142
                             + "<script type=\"text/javascript\">\n"
143
                             + "var yasgui = YASGUI(document.getElementById(\"yasgui\"), {\n"
144
                             + "  yasqe:{sparql:{endpoint:'/repo/" + repo + "'},value:'" + Utils.defaultQuery.replaceAll("\n", "\\\\n") + "'}\n"
145
                             + "});\n"
146
                             + "</script>\n"
147
                             + "</body>\n"
148
                             + "</html>");
149
            } else {
150
                req.response()
151
                        .putHeader("content-type", "text/plain")
152
                        .setStatusCode(404)
153
                        .end("not found");
154
            }
155
        });
156
        proxyRouter.route(HttpMethod.GET, "/page").handler(req -> handleRedirect(req, "/page"));
157
        proxyRouter.route(HttpMethod.GET, "/page/*").handler(req -> {
158
            final String pagePattern = "^/page/([a-zA-Z0-9-_]+)(/([a-zA-Z0-9-_]+))?$";
159
            if (req.normalizedPath().matches(pagePattern)) {
160
                String repo = req.normalizedPath().replaceFirst(pagePattern, "$1$2");
161
                req.response()
162
                        .putHeader("content-type", "text/html")
163
                        .end("<!DOCTYPE html>\n"
164
                             + "<html lang=\"en\">\n"
165
                             + "<head>\n"
166
                             + "<meta charset=\"utf-8\">\n"
167
                             + "<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n"
168
                             + "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n"
169
                             + "<title>Nanopub Query repo: " + repo + "</title>\n"
170
                             + "<link rel=\"stylesheet\" href=\"/style.css\">\n"
171
                             + "</head>\n"
172
                             + "<body>\n"
173
                             + "<h3>Nanopub Query repo: " + repo + "</h3>\n"
174
                             + "<p>Endpoint: <a href=\"/repo/" + repo + "\">/repo/" + repo + "</a></p>"
175
                             + "<p>YASGUI: <a href=\"/tools/" + repo + "/yasgui.html\">/tools/" + repo + "/yasgui.hml</a></p>"
176
                             + "</body>\n"
177
                             + "</html>");
178
            } else {
179
                req.response()
180
                        .putHeader("content-type", "text/plain")
181
                        .setStatusCode(404)
182
                        .end("not found");
183
            }
184
        });
185
        proxyRouter.route(HttpMethod.GET, "/").handler(req -> {
186
            vertx.<String>executeBlocking(() -> {
187
                String repos = "";
188
                List<String> repoList = new ArrayList<>(TripleStore.get().getRepositoryNames());
189
                Collections.sort(repoList);
190
                for (String s : repoList) {
191
                    if (s.startsWith("pubkey_") || s.startsWith("type_")) continue;
192
                    repos += "<li><code><a href=\"/page/" + s + "\">" + s + "</a></code></li>";
193
                }
194
                String pinnedApisValue = Utils.getEnvString("NANOPUB_QUERY_PINNED_APIS", "");
195
                String[] pinnedApis = pinnedApisValue.split(" ");
196
                String pinnedApiLinks = "";
197
                if (!pinnedApisValue.isEmpty()) {
198
                    for (String s : pinnedApis) {
199
                        pinnedApiLinks = pinnedApiLinks + "<li><a href=\"openapi/?url=spec/" + s + "%3Fapi-version=latest\">" + s.replaceFirst("^.*/", "") + "</a></li>";
200
                    }
201
                    pinnedApiLinks = "<p>Pinned APIs:</p>\n" +
202
                                     "<ul>\n" +
203
                                     pinnedApiLinks +
204
                                     "</ul>\n";
205
                }
206
                return "<!DOCTYPE html>\n"
207
                     + "<html lang='en'>\n"
208
                     + "<head>\n"
209
                     + "<title>Nanopub Query</title>\n"
210
                     + "<meta charset='utf-8'>\n"
211
                     + "<link rel=\"stylesheet\" href=\"/style.css\">\n"
212
                     + "</head>\n"
213
                     + "<body>\n"
214
                     + "<h1>Nanopub Query</h1>"
215
                     + "<p>General repos:</p>"
216
                     + "<ul>" + repos + "</ul>"
217
                     + "<p>Specific repos:</p>"
218
                     + "<ul>"
219
                     + "<li><a href=\"/pubkeys\">Pubkey Repos</a></li>"
220
                     + "<li><a href=\"/types\">Type Repos</a></li>"
221
                     + "</ul>"
222
                     + pinnedApiLinks
223
                     + "</body>\n"
224
                     + "</html>";
225
            }, false).onSuccess(html -> {
226
                req.response().putHeader("content-type", "text/html").end(html);
227
            }).onFailure(ex -> {
228
                req.response().setStatusCode(500).end("Error: " + ex.getMessage());
229
            });
230
        });
231
        proxyRouter.route(HttpMethod.GET, "/pubkeys").handler(req -> {
232
            vertx.<String>executeBlocking(() -> {
233
                String repos = "";
234
                List<String> repoList = new ArrayList<>(TripleStore.get().getRepositoryNames());
235
                Collections.sort(repoList);
236
                for (String s : repoList) {
237
                    if (!s.startsWith("pubkey_")) continue;
238
                    String hash = s.replaceFirst("^([a-zA-Z0-9-]+)_([a-zA-Z0-9-_]+)$", "$2");
239
                    Value hashObj = Utils.getObjectForHash(hash);
240
                    String label;
241
                    if (hashObj == null) {
242
                        label = "";
243
                    } else {
244
                        label = " (" + Utils.getShortPubkeyName(hashObj.stringValue()) + ")";
245
                    }
246
                    s = s.replaceFirst("^([a-zA-Z0-9-]+)_([a-zA-Z0-9-_]+)$", "$1/$2");
247
                    repos += "<li><code><a href=\"/page/" + s + "\">" + s + "</a>" + label + "</code></li>";
248
                }
249
                return "<!DOCTYPE html>\n"
250
                     + "<html lang='en'>\n"
251
                     + "<head>\n"
252
                     + "<title>Nanopub Query: Pubkey Repos</title>\n"
253
                     + "<meta charset='utf-8'>\n"
254
                     + "<link rel=\"stylesheet\" href=\"/style.css\">\n"
255
                     + "</head>\n"
256
                     + "<body>\n"
257
                     + "<h3>Pubkey Repos</h3>"
258
                     + "<p>Repos:</p>"
259
                     + "<ul>" + repos + "</ul>"
260
                     + "</body>\n"
261
                     + "</html>";
262
            }, false).onSuccess(html -> {
263
                req.response().putHeader("content-type", "text/html").end(html);
264
            }).onFailure(ex -> {
265
                req.response().setStatusCode(500).end("Error: " + ex.getMessage());
266
            });
267
        });
268
        proxyRouter.route(HttpMethod.GET, "/types").handler(req -> {
269
            vertx.<String>executeBlocking(() -> {
270
                String repos = "";
271
                List<String> repoList = new ArrayList<>(TripleStore.get().getRepositoryNames());
272
                Collections.sort(repoList);
273
                for (String s : repoList) {
274
                    if (!s.startsWith("type_")) continue;
275
                    String hash = s.replaceFirst("^([a-zA-Z0-9-]+)_([a-zA-Z0-9-_]+)$", "$2");
276
                    Value hashObj = Utils.getObjectForHash(hash);
277
                    String label;
278
                    if (hashObj == null) {
279
                        label = "";
280
                    } else {
281
                        label = " (" + hashObj.stringValue() + ")";
282
                    }
283
                    s = s.replaceFirst("^([a-zA-Z0-9-]+)_([a-zA-Z0-9-_]+)$", "$1/$2");
284
                    repos += "<li><code><a href=\"/page/" + s + "\">" + s + "</a>" + label + "</code></li>";
285
                }
286
                return "<!DOCTYPE html>\n"
287
                     + "<html lang='en'>\n"
288
                     + "<head>\n"
289
                     + "<title>Nanopub Query: Type Repos</title>\n"
290
                     + "<meta charset='utf-8'>\n"
291
                     + "<link rel=\"stylesheet\" href=\"/style.css\">\n"
292
                     + "</head>\n"
293
                     + "<body>\n"
294
                     + "<h3>Type Repos</h3>"
295
                     + "<p>Repos:</p>"
296
                     + "<ul>" + repos + "</ul>"
297
                     + "</body>\n"
298
                     + "</html>";
299
            }, false).onSuccess(html -> {
300
                req.response().putHeader("content-type", "text/html").end(html);
301
            }).onFailure(ex -> {
302
                req.response().setStatusCode(500).end("Error: " + ex.getMessage());
303
            });
304
        });
305
        proxyRouter.route(HttpMethod.GET, "/style.css").handler(req -> {
306
            if (css == null) {
307
                css = getResourceAsString("style.css");
308
            }
309
            req.response().end(css);
310
        });
311

312
        // TODO This is no longer needed and can be removed at some point:
313
        proxyRouter.route(HttpMethod.GET, "/grlc-spec/*").handler(req -> {
314
            vertx.<String>executeBlocking(() -> {
315
                GrlcSpec gsp = new GrlcSpec(req.normalizedPath(), req.queryParams());
316
                return gsp.getSpec();
317
            }, false).onSuccess(spec -> {
318
                req.response().putHeader("content-type", "text/yaml").end(spec);
319
            }).onFailure(ex -> {
320
                if (ex instanceof InvalidGrlcSpecException) {
321
                    req.response().setStatusCode(400).end(ex.getMessage());
322
                } else {
323
                    req.response().setStatusCode(500).end("Unexpected error: " + ex.getMessage());
324
                }
325
            });
326
        });
327

328
        proxyRouter.route(HttpMethod.GET, "/openapi/spec/*").handler(req -> {
329
            vertx.<String>executeBlocking(() -> {
330
                OpenApiSpecPage osp = new OpenApiSpecPage(req.normalizedPath(), req.queryParams());
331
                return osp.getSpec();
332
            }, false).onSuccess(spec -> {
333
                req.response().putHeader("content-type", "text/yaml").end(spec);
334
            }).onFailure(ex -> {
335
                if (ex instanceof InvalidGrlcSpecException) {
336
                    req.response().setStatusCode(400).end("Invalid grlc API definition: " + ex.getMessage());
337
                } else {
338
                    req.response().setStatusCode(500).end("Unexpected error: " + ex.getMessage());
339
                }
340
            });
341
        });
342

343
        proxyRouter.route("/openapi/*").handler(StaticHandler.create("com/knowledgepixels/query/swagger"));
344

345
        HttpProxy grlcxProxy = HttpProxy.reverseProxy(httpClient);
346
        grlcxProxy.origin(proxyPort, proxy);
347

348
        grlcxProxy.addInterceptor(new ProxyInterceptor() {
×
349

350
            @Override
351
            @GeneratedFlagForDependentElements
352
            public Future<ProxyResponse> handleProxyRequest(ProxyContext context) {
353
                final ProxyRequest req = context.request();
354
                final String apiPattern = "^/api/(RA[a-zA-Z0-9-_]{43})/([a-zA-Z0-9-_]+)([.]csv|[.]json|[.]srx)?([?].*)?$";
355
                if (req.getURI().matches(apiPattern)) {
356
                    try {
357
                        req.setMethod(HttpMethod.POST);
358
                        if (req.getURI().matches(".*[.]csv([?].*)?$")) {
359
                            req.putHeader("Accept", "text/csv");
360
                            req.setURI(req.getURI().replaceFirst("[.]csv([?].*)?$", "$1"));
361
                        } else if (req.getURI().matches(".*[.]json([?].*)?$")) {
362
                            req.putHeader("Accept", "application/json");
363
                            req.setURI(req.getURI().replaceFirst("[.]json([?].*)?$", "$1"));
364
                        } else if (req.getURI().matches(".*[.]srx([?].*)?$")) {
365
                            req.putHeader("Accept", "application/xml");
366
                            req.setURI(req.getURI().replaceFirst("[.]srx([?].*)?$", "$1"));
367
                        }
368
                        GrlcSpec grlcSpec = new GrlcSpec(req.getURI(), req.proxiedRequest().params());
369

370
                        // Variant 1:
371
                        req.putHeader("Content-Type", "application/sparql-query");
372
                        req.setBody(Body.body(Buffer.buffer(grlcSpec.expandQuery())));
373
                        // Variant 2:
374
                        //req.putHeader("Content-Type", "application/x-www-form-urlencoded");
375
                        //req.setBody(Body.body(Buffer.buffer("query=" + URLEncoder.encode(grlcSpec.getExpandedQueryContent(), Charsets.UTF_8))));
376

377
                        req.setURI("/rdf4j-server/repositories/" + grlcSpec.getRepoName());
378
                        log.info("Forwarding apix request to /rdf4j-server/repositories/", grlcSpec.getRepoName());
379
                    } catch (InvalidGrlcSpecException ex) {
380
                        return Future.succeededFuture(context.request()
381
                                .response()
382
                                .setStatusCode(400)
383
                                .putHeader("Content-Type", "text/plain")
384
                                .setBody(Body.body(Buffer.buffer("Bad request: " + ex.getMessage()))));
385
                    } catch (Exception ex) {
386
                        return Future.succeededFuture(context.request()
387
                                .response()
388
                                .setStatusCode(500)
389
                                .putHeader("Content-Type", "text/plain")
390
                                .setBody(Body.body(Buffer.buffer("Unexpected error: " + ex.getMessage()))));
391
                    }
392
                }
393
                return ProxyInterceptor.super.handleProxyRequest(context);
394
            }
395

396
            @Override
397
            @GeneratedFlagForDependentElements
398
            public Future<Void> handleProxyResponse(ProxyContext context) {
399
                log.info("Receiving api response");
400
                ProxyResponse resp = context.response();
401
                resp.putHeader("Access-Control-Allow-Origin", "*");
402
                resp.putHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
403
                resp.putHeader("Content-Disposition", "inline");
404
                return ProxyInterceptor.super.handleProxyResponse(context);
405
            }
406

407
        });
408
        proxyRouter.route(HttpMethod.GET, "/api/*").handler(ProxyHandler.create(grlcxProxy));
409

410
        // Handle HEAD requests for all paths not already covered (e.g. /repo/* has its own HEAD handler).
411
        // Global headers are applied before routing, so we just end the response with no body.
412
        proxyRouter.route(HttpMethod.HEAD, "/*").handler(req -> {
413
            req.response().setStatusCode(200).end();
414
        });
415

416
        proxyServer.requestHandler(req -> {
417
            applyGlobalHeaders(req.response());
418
            proxyRouter.handle(req);
419
        });
420
        proxyServer.listen(9393);
421

422
        // Periodic metrics update
423
        vertx.setPeriodic(1000, id -> collector.updateMetrics());
424

425

426
        new Thread(() -> {
427
            try {
428
                var status = StatusController.get().initialize();
429
                log.info("Current state: {}, last committed counter: {}", status.state, status.loadCounter);
430
                // Restore or fetch the registry setup ID
431
                Long storedSetupId = StatusController.get().getRegistrySetupId();
432
                if (storedSetupId != null) {
433
                    JellyNanopubLoader.setLastKnownSetupId(storedSetupId);
434
                    log.info("Restored registry setupId: {}", storedSetupId);
435
                } else if (status.state == StatusController.State.LAUNCHING
436
                        || status.state == StatusController.State.LOADING_INITIAL) {
437
                    // Fresh start or crashed during initial load – safe to adopt the current setupId
438
                    try {
439
                        var metadata = JellyNanopubLoader.fetchRegistryMetadata();
440
                        JellyNanopubLoader.setLastKnownSetupId(metadata.setupId());
441
                        if (metadata.setupId() != null) {
442
                            StatusController.get().setRegistrySetupId(metadata.setupId());
443
                            log.info("Fetched initial registry setupId: {}", metadata.setupId());
444
                        }
445
                    } catch (Exception e) {
446
                        log.warn("Could not fetch initial registry setupId", e);
447
                    }
448
                } else {
449
                    // Upgrade from a version without setupId tracking. The DB has data but
450
                    // we can't verify it matches the current registry state. Leave lastKnownSetupId
451
                    // as null so that loadUpdates() will trigger a resync.
452
                    log.warn("No stored registry setupId but DB has data (state: {}, counter: {}). "
453
                            + "A resync will be triggered on the first update poll.",
454
                            status.state, status.loadCounter);
455
                }
456
                boolean forceResync = "true".equalsIgnoreCase(
457
                        Utils.getEnvString("FORCE_RESYNC", "false"));
458
                if (forceResync && status.state != StatusController.State.LAUNCHING) {
459
                    log.warn("FORCE_RESYNC is set. Forcing full re-load from registry.");
460
                    var metadata = JellyNanopubLoader.fetchRegistryMetadata();
461
                    JellyNanopubLoader.setLastKnownSetupId(metadata.setupId());
462
                    if (metadata.setupId() != null) {
463
                        StatusController.get().setRegistrySetupId(metadata.setupId());
464
                    }
465
                    StatusController.get().setResetting();
466
                    StatusController.get().setLoadingInitial(-1);
467
                    JellyNanopubLoader.loadInitial(-1);
468
                    StatusController.get().setReady();
469
                } else if (status.state == StatusController.State.LAUNCHING || status.state == StatusController.State.LOADING_INITIAL) {
470
                    // Do the initial nanopublication loading
471
                    StatusController.get().setLoadingInitial(status.loadCounter);
472
                    // Fall back to local nanopub loading if the local files are present
473
                    if (!LocalNanopubLoader.init()) {
474
                        JellyNanopubLoader.loadInitial(status.loadCounter);
475
                    } else {
476
                        log.info("Local nanopublication loading finished");
477
                    }
478
                    StatusController.get().setReady();
479
                } else {
480
                    log.info("Initial load is already done");
481
                    StatusController.get().setReady();
482
                }
483
            } catch (Exception ex) {
484
                log.info("Initial load failed, terminating...", ex);
485
                Runtime.getRuntime().exit(1);
486
            }
487

488
            // Start periodic nanopub loading
489
            log.info("Starting periodic nanopub loading...");
490
            var executor = Executors.newSingleThreadScheduledExecutor();
491
            executor.scheduleWithFixedDelay(
492
                    JellyNanopubLoader::loadUpdates,
493
                    JellyNanopubLoader.UPDATES_POLL_INTERVAL,
494
                    JellyNanopubLoader.UPDATES_POLL_INTERVAL,
495
                    TimeUnit.MILLISECONDS
496
            );
497
        }).start();
498

499
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
500
            try {
501
                log.info("Gracefully shutting down...");
502
                TripleStore.get().shutdownRepositories();
503
                vertx.close().toCompletionStage().toCompletableFuture().get(5, TimeUnit.SECONDS);
504
                log.info("Graceful shutdown completed");
505
            } catch (Exception ex) {
506
                log.info("Graceful shutdown failed", ex);
507
            }
508
        }));
509
    }
510

511
    private String getResourceAsString(String file) {
512
        InputStream is = getClass().getClassLoader().getResourceAsStream("com/knowledgepixels/query/" + file);
513
        try (Scanner s = new Scanner(is).useDelimiter("\\A")) {
514
            String fileContent = s.hasNext() ? s.next() : "";
515
            return fileContent;
516
        }
517
    }
518

519
    private static void handleRedirect(RoutingContext req, String path) {
520
        String queryString = "";
521
        if (!req.queryParam("query").isEmpty())
522
            queryString = "?query=" + URLEncoder.encode(req.queryParam("query").getFirst(), Charsets.UTF_8);
523
        if (req.queryParam("for-type").size() == 1) {
524
            String type = req.queryParam("for-type").getFirst();
525
            req.response().putHeader("location", path + "/type/" + Utils.createHash(type) + queryString);
526
            req.response().setStatusCode(301).end();
527
        } else if (req.queryParam("for-pubkey").size() == 1) {
528
            String type = req.queryParam("for-pubkey").getFirst();
529
            req.response().putHeader("location", path + "/pubkey/" + Utils.createHash(type) + queryString);
530
            req.response().setStatusCode(301).end();
531
        } else if (req.queryParam("for-user").size() == 1) {
532
            String type = req.queryParam("for-user").getFirst();
533
            req.response().putHeader("location", path + "/user/" + Utils.createHash(type) + queryString);
534
            req.response().setStatusCode(301).end();
535
        }
536
    }
537

538
    /**
539
     * Apply headers to the response that should be present for all requests.
540
     *
541
     * @param response The response to which the headers should be applied.
542
     */
543
    static void applyGlobalHeaders(HttpServerResponse response) {
544
        var state = StatusController.get().getState();
545
        response.putHeader("Nanopub-Query-Status", state.state.toString());
546
        response.putHeader("Nanopub-Query-Registry-Url", JellyNanopubLoader.registryUrl);
547
        Long setupId = StatusController.get().getRegistrySetupId();
548
        response.putHeader("Nanopub-Query-Registry-Setup-Id", setupId == null ? "" : setupId.toString());
549
        response.putHeader("Nanopub-Query-Load-Counter", String.valueOf(state.loadCounter));
550
    }
551
}
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