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

knowledgepixels / nanopub-registry / 28439664189

30 Jun 2026 11:05AM UTC coverage: 32.308%. Remained the same
28439664189

Pull #121

github

web-flow
Merge 2456050b0 into cf73b1dd3
Pull Request #121: chore(update): shorten trust-state update interval from 1h to 10min

319 of 1116 branches covered (28.58%)

Branch coverage included in aggregate %.

1068 of 3177 relevant lines covered (33.62%)

5.57 hits per line

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

11.75
src/main/java/com/knowledgepixels/registry/Task.java
1
package com.knowledgepixels.registry;
2

3
import com.knowledgepixels.registry.db.IndexInitializer;
4
import com.mongodb.client.ClientSession;
5
import com.mongodb.client.FindIterable;
6
import com.mongodb.client.MongoCollection;
7
import com.mongodb.client.MongoCursor;
8
import com.mongodb.client.model.ReplaceOptions;
9
import net.trustyuri.TrustyUriUtils;
10
import org.apache.commons.lang.Validate;
11
import org.bson.Document;
12
import org.eclipse.rdf4j.model.IRI;
13
import org.eclipse.rdf4j.model.Statement;
14
import org.nanopub.Nanopub;
15
import org.nanopub.SimpleTimestampPattern;
16
import org.nanopub.extra.index.IndexUtils;
17
import org.nanopub.extra.index.NanopubIndex;
18
import org.nanopub.extra.security.KeyDeclaration;
19
import org.nanopub.extra.setting.IntroNanopub;
20
import org.nanopub.extra.setting.NanopubSetting;
21
import org.slf4j.Logger;
22
import org.slf4j.LoggerFactory;
23

24
import java.io.Serializable;
25
import java.time.ZonedDateTime;
26
import java.util.*;
27
import java.util.concurrent.atomic.AtomicLong;
28

29
import static com.knowledgepixels.registry.EntryStatus.*;
30
import static com.knowledgepixels.registry.NanopubLoader.*;
31
import static com.knowledgepixels.registry.RegistryDB.*;
32
import static com.knowledgepixels.registry.ServerStatus.*;
33
import static com.mongodb.client.model.Filters.eq;
34
import static com.mongodb.client.model.Sorts.*;
35

36
public enum Task implements Serializable {
6✔
37

38
    INIT_DB {
33✔
39
        public void run(ClientSession s, Document taskDoc) {
40
            logger.info("Running INIT_DB task: initializing a fresh database");
9✔
41
            setServerStatus(s, launching);
9✔
42

43
            increaseStateCounter(s);
6✔
44
            if (RegistryDB.isInitialized(s)) {
9!
45
                logger.error("INIT_DB aborted: database is already initialized");
×
46
                throw new RuntimeException("DB already initialized");
×
47
            }
48

49
            long setupId = Math.abs(Utils.getRandom().nextLong());
12✔
50
            boolean testInstance = "true".equals(System.getenv("REGISTRY_TEST_INSTANCE"));
15✔
51
            logger.info("Initializing new database with setupId={} (testInstance={})", setupId, testInstance);
21✔
52

53
            setValue(s, Collection.SERVER_INFO.toString(), "setupId", setupId);
21✔
54
            setValue(s, Collection.SERVER_INFO.toString(), "testInstance", testInstance);
21✔
55

56
            logger.debug("INIT_DB complete; scheduling LOAD_CONFIG task");
9✔
57
            schedule(s, LOAD_CONFIG);
9✔
58
        }
3✔
59

60
    },
61

62
    LOAD_CONFIG {
33✔
63
        public void run(ClientSession s, Document taskDoc) {
64
            logger.info("Running LOAD_CONFIG task");
9✔
65
            ServerStatus status = getServerStatus(s);
9✔
66
            if (status != launching) {
9!
67
                logger.error("LOAD_CONFIG aborted: expected server status '{}' but found '{}'", launching, status);
×
68
                throw new IllegalTaskStatusException("Illegal status for this task: " + status);
×
69
            }
70

71
            String coverageTypes = System.getenv("REGISTRY_COVERAGE_TYPES");
9✔
72
            if (coverageTypes != null) {
6!
73
                logger.info("Setting coverageTypes from REGISTRY_COVERAGE_TYPES: {}", coverageTypes);
×
74
                setValue(s, Collection.SERVER_INFO.toString(), "coverageTypes", coverageTypes);
×
75
            } else {
76
                logger.debug("REGISTRY_COVERAGE_TYPES not set; leaving coverageTypes unset");
9✔
77
            }
78

79
            String coverageAgents = System.getenv("REGISTRY_COVERAGE_AGENTS");
9✔
80
            if (coverageAgents != null) {
6!
81
                logger.info("Setting coverageAgents from REGISTRY_COVERAGE_AGENTS: {}", coverageAgents);
×
82
                setValue(s, Collection.SERVER_INFO.toString(), "coverageAgents", coverageAgents);
×
83
            } else {
84
                logger.debug("REGISTRY_COVERAGE_AGENTS not set; leaving coverageAgents unset");
9✔
85
            }
86

87
            logger.debug("LOAD_CONFIG complete; scheduling LOAD_SETTING task");
9✔
88
            schedule(s, LOAD_SETTING);
9✔
89
        }
3✔
90

91
    },
92

93
    LOAD_SETTING {
33✔
94
        public void run(ClientSession s, Document taskDoc) throws Exception {
95
            logger.info("Running LOAD_SETTING task");
9✔
96
            ServerStatus status = getServerStatus(s);
9✔
97
            if (status != launching) {
9!
98
                logger.error("LOAD_SETTING aborted: expected server status '{}' but found '{}'", launching, status);
×
99
                throw new IllegalTaskStatusException("Illegal status for this task: " + status);
×
100
            }
101

102
            NanopubSetting settingNp = Utils.getSetting();
6✔
103
            String settingId = TrustyUriUtils.getArtifactCode(settingNp.getNanopub().getUri().stringValue());
18✔
104
            logger.info("Loading setting nanopub {}", settingId);
12✔
105
            setValue(s, Collection.SETTING.toString(), "original", settingId);
18✔
106
            setValue(s, Collection.SETTING.toString(), "current", settingId);
18✔
107
            loadNanopub(s, settingNp.getNanopub());
15✔
108

109
            List<Document> bootstrapServices = new ArrayList<>();
12✔
110
            for (IRI i : settingNp.getBootstrapServices()) {
33✔
111
                bootstrapServices.add(new Document("_id", i.stringValue()));
27✔
112
            }
3✔
113
            logger.info("Setting {} bootstrap service(s) from the setting nanopub: {}", bootstrapServices.size(), bootstrapServices);
21✔
114
            // potentially currently hardcoded in the nanopub lib
115
            setValue(s, Collection.SETTING.toString(), "bootstrap-services", bootstrapServices);
18✔
116

117
            boolean performFullLoad = !"false".equals(System.getenv("REGISTRY_PERFORM_FULL_LOAD"));
24!
118
            if (performFullLoad) {
6!
119
                logger.debug("REGISTRY_PERFORM_FULL_LOAD not disabled; scheduling LOAD_FULL task");
9✔
120
                schedule(s, LOAD_FULL);
12✔
121
            } else {
122
                logger.info("REGISTRY_PERFORM_FULL_LOAD=false; skipping LOAD_FULL task");
×
123
            }
124

125
            logger.debug("LOAD_SETTING complete; transitioning server status to '{}' and scheduling INIT_COLLECTIONS", coreLoading);
12✔
126
            setServerStatus(s, coreLoading);
9✔
127
            schedule(s, INIT_COLLECTIONS);
9✔
128
        }
3✔
129
    },
130

131
    INIT_COLLECTIONS {
33✔
132

133
        // DB read from:
134
        // DB write to:  trustPaths, endorsements, accounts
135
        // This state is periodically executed
136

137
        public void run(ClientSession s, Document taskDoc) throws Exception {
138
            logger.info("Running INIT_COLLECTIONS task");
×
139
            ServerStatus status = getServerStatus(s);
×
140
            if (status != coreLoading && status != updating) {
×
141
                logger.error("INIT_COLLECTIONS aborted: expected server status 'coreLoading' or 'updating' but found '{}'", status);
×
142
                throw new IllegalTaskStatusException("Illegal status for this task: " + status);
×
143
            }
144

145
            IndexInitializer.initLoadingCollections(s);
×
146

147
            if ("false".equals(System.getenv("REGISTRY_ENABLE_TRUST_CALCULATION"))) {
×
148
                logger.info("Trust calculation disabled (REGISTRY_ENABLE_TRUST_CALCULATION=false); skipping to FINALIZE_TRUST_STATE");
×
149
                for (Map.Entry<String, Integer> entry : AgentFilter.getExplicitPubkeys().entrySet()) {
×
150
                    String pubkeyHash = entry.getKey();
×
151
                    int quota = entry.getValue();
×
152
                    Document account = new Document("agent", "")
×
153
                            .append("pubkey", pubkeyHash)
×
154
                            .append("status", toLoad.getValue())
×
155
                            .append("depth", 0)
×
156
                            .append("quota", quota);
×
157
                    if (!has(s, "accounts_loading", new Document("pubkey", pubkeyHash))) {
×
158
                        insert(s, "accounts_loading", account);
×
159
                        logger.debug("Seeded explicit pubkey as account: {} (quota={})", pubkeyHash, quota);
×
160
                    }
161
                }
×
162
                schedule(s, FINALIZE_TRUST_STATE);
×
163
                return;
×
164
            }
165

166
            logger.debug("Inserting root trust path entry for base agent");
×
167
            // since this may take long, we start with postfix "_loading"
168
            // and only at completion it's changed to trustPath, endorsements, accounts
169
            insert(s, "trustPaths_loading",
×
170
                    new Document("_id", "$")
171
                            .append("sorthash", "")
×
172
                            .append("agent", "$")
×
173
                            .append("pubkey", "$")
×
174
                            .append("depth", 0)
×
175
                            .append("ratio", 1.0d)
×
176
                            .append("type", "extended")
×
177
            );
178

179
            String agentIntroCollectionUri = Utils.getSetting().getAgentIntroCollection().stringValue();
×
180
            logger.info("Retrieving base agent intro collection: {}", agentIntroCollectionUri);
×
181
            NanopubIndex agentIndex = IndexUtils.castToIndex(NanopubLoader.retrieveNanopub(s, agentIntroCollectionUri));
×
182
            loadNanopub(s, agentIndex);
×
183
            for (IRI el : agentIndex.getElements()) {
×
184
                String declarationAc = TrustyUriUtils.getArtifactCode(el.stringValue());
×
185
                Validate.notNull(declarationAc);
×
186

187
                insert(s, "endorsements_loading",
×
188
                        new Document("agent", "$")
189
                                .append("pubkey", "$")
×
190
                                .append("endorsedNanopub", declarationAc)
×
191
                                .append("source", getValue(s, Collection.SETTING.toString(), "current").toString())
×
192
                                .append("status", toRetrieve.getValue())
×
193

194
                );
195

196
            }
×
197
            insert(s, "accounts_loading",
×
198
                    new Document("agent", "$")
199
                            .append("pubkey", "$")
×
200
                            .append("status", visited.getValue())
×
201
                            .append("depth", 0)
×
202
            );
203

204
            logger.info("Starting iteration at depth 0");
×
205
            schedule(s, LOAD_DECLARATIONS.with("depth", 1));
×
206
        }
×
207

208
        // At the end of this task, the base agent is initialized:
209
        // ------------------------------------------------------------
210
        //
211
        //              $$$$ ----endorses----> [intro]
212
        //              base                (to-retrieve)
213
        //              $$$$
214
        //            (visited)
215
        //
216
        //              [0] trust path
217
        //
218
        // ------------------------------------------------------------
219
        // Only one endorses-link to an introduction is shown here,
220
        // but there are typically several.
221

222
    },
223

224
    LOAD_DECLARATIONS {
33✔
225

226
        // In general, we have at this point accounts with
227
        // endorsement links to unvisited agent introductions:
228
        // ------------------------------------------------------------
229
        //
230
        //         o      ----endorses----> [intro]
231
        //    --> /#\  /o\___            (to-retrieve)
232
        //        / \  \_/^^^
233
        //         (visited)
234
        //
235
        //    ========[X] trust path
236
        //
237
        // ------------------------------------------------------------
238

239
        // DB read from: endorsements, trustEdges, accounts
240
        // DB write to:  endorsements, trustEdges, accounts
241

242
        public void run(ClientSession s, Document taskDoc) {
243

244
            int depth = taskDoc.getInteger("depth");
×
245
            logger.info("Running LOAD_DECLARATIONS task at depth {}", depth);
×
246

247
            while (true) {
248
                Document d = getOne(s, "endorsements_loading",
×
249
                        new DbEntryWrapper(toRetrieve).getDocument());
×
250
                if (d == null) {
×
251
                    break;
×
252
                }
253

254
                IntroNanopub agentIntro = getAgentIntro(s, d.getString("endorsedNanopub"));
×
255
                if (agentIntro != null) {
×
256
                    String agentId = agentIntro.getUser().stringValue();
×
257
                    // foaf:name + dct:created of the intro nanopub. Same name applies to every
258
                    // KeyDeclaration in the intro, so resolve once outside the inner loop.
259
                    String introName = Utils.extractIntroName(agentIntro);
×
260
                    Calendar introCreatedCal = SimpleTimestampPattern.getCreationTime(agentIntro.getNanopub());
×
261
                    Date introCreatedAt = (introCreatedCal == null) ? null : introCreatedCal.getTime();
×
262
                    // The authorizing introduction for this (agent, pubkey): it carries the
263
                    // KeyDeclaration for the pubkey. Recorded alongside the name and kept in sync
264
                    // with it (latest-dct:created wins), so it reflects one authorizing intro —
265
                    // the name-winning one — not the complete set of intros for this account.
266
                    String introNp = agentIntro.getNanopub().getUri().stringValue();
×
267

268
                    for (KeyDeclaration kd : agentIntro.getKeyDeclarations()) {
×
269
                        String sourceAgent = d.getString("agent");
×
270
                        Validate.notNull(sourceAgent);
×
271
                        String sourcePubkey = d.getString("pubkey");
×
272
                        Validate.notNull(sourcePubkey);
×
273
                        String sourceAc = d.getString("source");
×
274
                        Validate.notNull(sourceAc);
×
275
                        String agentPubkey = Utils.getHash(kd.getPublicKeyString());
×
276
                        Validate.notNull(agentPubkey);
×
277
                        Document trustEdge = new Document("fromAgent", sourceAgent)
×
278
                                .append("fromPubkey", sourcePubkey)
×
279
                                .append("toAgent", agentId)
×
280
                                .append("toPubkey", agentPubkey)
×
281
                                .append("source", sourceAc);
×
282
                        if (!has(s, "trustEdges", trustEdge)) {
×
283
                            boolean invalidated = has(s, "invalidations", new Document("invalidatedNp", sourceAc).append("invalidatingPubkey", sourcePubkey));
×
284
                            if (invalidated) {
×
285
                                logger.info("Trust edge from {}/{} to {}/{} is invalidated by source {}", sourceAgent, sourcePubkey, agentId, agentPubkey, sourceAc);
×
286
                            }
287
                            insert(s, "trustEdges", trustEdge.append("invalidated", invalidated));
×
288
                        }
289

290
                        Document agent = new Document("agent", agentId).append("pubkey", agentPubkey);
×
291
                        Document existing = collection("accounts_loading").find(s, agent).first();
×
292
                        if (existing == null) {
×
293
                            insert(s, "accounts_loading", agent
×
294
                                    .append("status", seen.getValue())
×
295
                                    .append("depth", depth)
×
296
                                    .append("name", introName)
×
297
                                    .append("nameCreatedAt", introCreatedAt)
×
298
                                    .append("introNanopub", introNp));
×
299
                        } else if (introName != null) {
×
300
                            // Per-(agent, pubkey) name policy: keep the name from the intro
301
                            // with the latest dct:created. First write wins when no current
302
                            // timestamp exists; otherwise compare and replace iff strictly newer.
303
                            Date currentCreatedAt = existing.getDate("nameCreatedAt");
×
304
                            if (currentCreatedAt == null
×
305
                                || (introCreatedAt != null && introCreatedAt.after(currentCreatedAt))) {
×
306
                                set(s, "accounts_loading", existing
×
307
                                        .append("name", introName)
×
308
                                        .append("nameCreatedAt", introCreatedAt)
×
309
                                        .append("introNanopub", introNp));
×
310
                            }
311
                        }
312
                    }
×
313

314
                    set(s, "endorsements_loading", d.append("status", retrieved.getValue()));
×
315
                } else {
×
316
                    logger.debug("Discarding endorsement {}: referenced nanopub {} is not a valid agent intro", d.get("_id"), d.getString("endorsedNanopub"));
×
317
                    set(s, "endorsements_loading", d.append("status", discarded.getValue()));
×
318
                }
319
            }
×
320
            logger.info("LOAD_DECLARATIONS at depth {} complete.", depth);
×
321
            schedule(s, EXPAND_TRUST_PATHS.with("depth", depth));
×
322
        }
×
323

324
        // At the end of this step, the key declarations in the agent
325
        // introductions are loaded and the corresponding trust edges
326
        // established:
327
        // ------------------------------------------------------------
328
        //
329
        //        o      ----endorses----> [intro]
330
        //   --> /#\  /o\___                o
331
        //       / \  \_/^^^ ---trusts---> /#\  /o\___
332
        //        (visited)                / \  \_/^^^
333
        //                                   (seen)
334
        //
335
        //   ========[X] trust path
336
        //
337
        // ------------------------------------------------------------
338
        // Only one trust edge per introduction is shown here, but
339
        // there can be several.
340

341
    },
342

343
    EXPAND_TRUST_PATHS {
33✔
344

345
        // DB read from: accounts, trustPaths, trustEdges
346
        // DB write to:  accounts, trustPaths
347

348
        public void run(ClientSession s, Document taskDoc) {
349

350
            int depth = taskDoc.getInteger("depth");
×
351
            logger.info("Running EXPAND_TRUST_PATHS task at depth {}", depth);
×
352

353
            while (true) {
354
                Document d = getOne(s, "accounts_loading",
×
355
                        new Document("status", visited.getValue())
×
356
                                .append("depth", depth - 1)
×
357
                );
358
                if (d == null) {
×
359
                    break;
×
360
                }
361

362
                String agentId = d.getString("agent");
×
363
                Validate.notNull(agentId);
×
364
                String pubkeyHash = d.getString("pubkey");
×
365
                Validate.notNull(pubkeyHash);
×
366

367
                Document trustPath = collection("trustPaths_loading").find(s,
×
368
                        new Document("agent", agentId).append("pubkey", pubkeyHash).append("type", "extended").append("depth", depth - 1)
×
369
                ).sort(orderBy(descending("ratio"), ascending("sorthash"))).first();
×
370

371
                if (trustPath == null) {
×
372
                    // Check it again in next iteration:
373
                    set(s, "accounts_loading", d.append("depth", depth));
×
374
                } else {
375
                    // Only first matching trust path is considered
376

377
                    Map<String, Document> newPaths = new HashMap<>();
×
378
                    Map<String, Set<String>> pubkeySets = new HashMap<>();
×
379
                    String currentSetting = getValue(s, Collection.SETTING.toString(), "current").toString();
×
380

381
                    try (MongoCursor<Document> edgeCursor = get(s, "trustEdges",
×
382
                            new Document("fromAgent", agentId)
383
                                    .append("fromPubkey", pubkeyHash)
×
384
                                    .append("invalidated", false)
×
385
                    )) {
386
                        while (edgeCursor.hasNext()) {
×
387
                            Document e = edgeCursor.next();
×
388

389
                            String agent = e.getString("toAgent");
×
390
                            Validate.notNull(agent);
×
391
                            String pubkey = e.getString("toPubkey");
×
392
                            Validate.notNull(pubkey);
×
393
                            String pathId = trustPath.getString("_id") + " " + agent + "|" + pubkey;
×
394
                            newPaths.put(pathId,
×
395
                                    new Document("_id", pathId)
396
                                            .append("sorthash", Utils.getHash(currentSetting + " " + pathId))
×
397
                                            .append("agent", agent)
×
398
                                            .append("pubkey", pubkey)
×
399
                                            .append("depth", depth)
×
400
                                            .append("type", "extended")
×
401
                            );
402
                            if (!pubkeySets.containsKey(agent)) {
×
403
                                pubkeySets.put(agent, new HashSet<>());
×
404
                            }
405
                            pubkeySets.get(agent).add(pubkey);
×
406
                        }
×
407
                    }
408
                    for (String pathId : newPaths.keySet()) {
×
409
                        Document pd = newPaths.get(pathId);
×
410
                        // first divide by agents; then for each agent, divide by number of pubkeys:
411
                        double newRatio = (trustPath.getDouble("ratio") * 0.9) / pubkeySets.size() / pubkeySets.get(pd.getString("agent")).size();
×
412
                        insert(s, "trustPaths_loading", pd.append("ratio", newRatio));
×
413
                    }
×
414
                    // Retain only 10% of the ratio — the other 90% was distributed to children
415
                    double retainedRatio = trustPath.getDouble("ratio") * 0.1;
×
416
                    set(s, "trustPaths_loading", trustPath.append("type", "primary").append("ratio", retainedRatio));
×
417
                    set(s, "accounts_loading", d.append("status", expanded.getValue()));
×
418
                }
419
            }
×
420
            logger.info("EXPAND_TRUST_PATHS at depth {} complete.", depth);
×
421
            schedule(s, LOAD_CORE.with("depth", depth).append("load-count", 0));
×
422
        }
×
423

424
        // At the end of this step, trust paths are updated to include
425
        // the new accounts:
426
        // ------------------------------------------------------------
427
        //
428
        //         o      ----endorses----> [intro]
429
        //    --> /#\  /o\___                o
430
        //        / \  \_/^^^ ---trusts---> /#\  /o\___
431
        //        (expanded)                / \  \_/^^^
432
        //                                    (seen)
433
        //
434
        //    ========[X]=====================[X+1] trust path
435
        //
436
        // ------------------------------------------------------------
437
        // Only one trust path is shown here, but they branch out if
438
        // several trust edges are present.
439

440
    },
441

442
    LOAD_CORE {
33✔
443

444
        // From here on, we refocus on the head of the trust paths:
445
        // ------------------------------------------------------------
446
        //
447
        //         o
448
        //    --> /#\  /o\___
449
        //        / \  \_/^^^
450
        //          (seen)
451
        //
452
        //    ========[X] trust path
453
        //
454
        // ------------------------------------------------------------
455

456
        // DB read from: accounts, trustPaths, endorsements, lists
457
        // DB write to:  accounts, endorsements, lists
458

459
        public void run(ClientSession s, Document taskDoc) {
460

461
            int depth = taskDoc.getInteger("depth");
×
462
            int loadCount = taskDoc.getInteger("load-count");
×
463

464
            Document agentAccount = getOne(s, "accounts_loading",
×
465
                    new Document("depth", depth).append("status", seen.getValue()));
×
466
            final String agentId;
467
            final String pubkeyHash;
468
            final Document trustPath;
469
            if (agentAccount != null) {
×
470
                agentId = agentAccount.getString("agent");
×
471
                Validate.notNull(agentId);
×
472
                pubkeyHash = agentAccount.getString("pubkey");
×
473
                Validate.notNull(pubkeyHash);
×
474
                trustPath = getOne(s, "trustPaths_loading",
×
475
                        new Document("depth", depth)
×
476
                                .append("agent", agentId)
×
477
                                .append("pubkey", pubkeyHash)
×
478
                );
479
            } else {
480
                agentId = null;
×
481
                pubkeyHash = null;
×
482
                trustPath = null;
×
483
            }
484

485
            if (agentAccount == null) {
×
486
                logger.info("LOAD_CORE at depth {} complete: {} account(s) processed", depth, loadCount);
×
487
                schedule(s, FINISH_ITERATION.with("depth", depth).append("load-count", loadCount));
×
488
            } else if (trustPath == null) {
×
489
                logger.debug("Account {}/{} has no trust path at depth {}; marking skipped", agentId, pubkeyHash, depth);
×
490
                // Account was seen but has no trust path at this depth; skip it
491
                set(s, "accounts_loading", agentAccount.append("status", skipped.getValue()));
×
492
                schedule(s, LOAD_CORE.with("depth", depth).append("load-count", loadCount));
×
493
            } else if (trustPath.getDouble("ratio") < MIN_TRUST_PATH_RATIO) {
×
494
                logger.debug("Pubkey {}: trust path ratio {} below minimum {}; skipping core load, marking encountered", pubkeyHash, trustPath.getDouble("ratio"), MIN_TRUST_PATH_RATIO);
×
495
                set(s, "accounts_loading", agentAccount.append("status", skipped.getValue()));
×
496
                Document d = new Document("pubkey", pubkeyHash).append("type", INTRO_TYPE_HASH);
×
497
                if (!has(s, "lists", d)) {
×
498
                    insert(s, "lists", d.append("status", encountered.getValue()));
×
499
                }
500
                schedule(s, LOAD_CORE.with("depth", depth).append("load-count", loadCount + 1));
×
501
            } else {
×
502
                logger.info("Pubkey {}: loading core (intro + endorsements) at depth {}", pubkeyHash, depth);
×
503
                // TODO check intro limit
504
                Document introList = new Document()
×
505
                        .append("pubkey", pubkeyHash)
×
506
                        .append("type", INTRO_TYPE_HASH)
×
507
                        .append("status", loading.getValue());
×
508
                if (!has(s, "lists", new Document("pubkey", pubkeyHash).append("type", INTRO_TYPE_HASH))) {
×
509
                    insert(s, "lists", introList);
×
510
                }
511

512
                // No checksum skip in LOAD_CORE: the endorsement extraction logic (below) needs to
513
                // see every nanopub to populate endorsements_loading, which is rebuilt from scratch each UPDATE.
514
                try (var stream = NanopubLoader.retrieveNanopubsFromPeers(INTRO_TYPE_HASH, pubkeyHash)) {
×
515
                    NanopubLoader.loadStreamInParallel(stream, np -> {
×
516
                        try (ClientSession ws = RegistryDB.getClient().startSession()) {
×
517
                            loadNanopub(ws, np, pubkeyHash, INTRO_TYPE);
×
518
                        }
519
                    });
×
520
                }
521

522
                set(s, "lists", introList.append("status", loaded.getValue()));
×
523
                logger.debug("Pubkey {}: intro list loaded", pubkeyHash);
×
524

525
                // TODO check endorsement limit
526
                Document endorseList = new Document()
×
527
                        .append("pubkey", pubkeyHash)
×
528
                        .append("type", ENDORSE_TYPE_HASH)
×
529
                        .append("status", loading.getValue());
×
530
                if (!has(s, "lists", new Document("pubkey", pubkeyHash).append("type", ENDORSE_TYPE_HASH))) {
×
531
                    insert(s, "lists", endorseList);
×
532
                }
533

534
                try (var stream = NanopubLoader.retrieveNanopubsFromPeers(ENDORSE_TYPE_HASH, pubkeyHash)) {
×
535
                    stream.forEach(m -> {
×
536
                        if (!m.isSuccess()) {
×
537
                            logger.error("Pubkey {}: failed to download an endorsement nanopub; aborting LOAD_CORE task", pubkeyHash);
×
538
                            throw new AbortingTaskException("Failed to download nanopub; aborting task...");
×
539
                        }
540
                        Nanopub nanopub = m.getNanopub();
×
541
                        loadNanopub(s, nanopub, pubkeyHash, ENDORSE_TYPE);
×
542
                        String sourceNpId = TrustyUriUtils.getArtifactCode(nanopub.getUri().stringValue());
×
543
                        Validate.notNull(sourceNpId);
×
544
                        for (Statement st : nanopub.getAssertion()) {
×
545
                            if (!st.getPredicate().equals(Utils.APPROVES_OF)) {
×
546
                                continue;
×
547
                            }
548
                            if (!(st.getObject() instanceof IRI)) {
×
549
                                continue;
×
550
                            }
551
                            if (!agentId.equals(st.getSubject().stringValue())) {
×
552
                                continue;
×
553
                            }
554
                            String objStr = st.getObject().stringValue();
×
555
                            if (!TrustyUriUtils.isPotentialTrustyUri(objStr)) {
×
556
                                continue;
×
557
                            }
558
                            String endorsedNpId = TrustyUriUtils.getArtifactCode(objStr);
×
559
                            Validate.notNull(endorsedNpId);
×
560
                            Document endorsement = new Document("agent", agentId)
×
561
                                    .append("pubkey", pubkeyHash)
×
562
                                    .append("endorsedNanopub", endorsedNpId)
×
563
                                    .append("source", sourceNpId);
×
564
                            if (!has(s, "endorsements_loading", endorsement)) {
×
565
                                insert(s, "endorsements_loading",
×
566
                                        endorsement.append("status", toRetrieve.getValue()));
×
567
                            }
568
                        }
×
569
                    });
×
570
                }
571
                logger.debug("Pubkey {}: endorsement list loaded", pubkeyHash);
×
572

573
                set(s, "lists", endorseList.append("status", loaded.getValue()));
×
574

575
                Document df = new Document("pubkey", pubkeyHash).append("type", "$");
×
576
                if (!has(s, "lists", df)) {
×
577
                    insert(s, "lists",
×
578
                            df.append("status", encountered.getValue()));
×
579
                }
580

581
                set(s, "accounts_loading", agentAccount.append("status", visited.getValue()));
×
582

583
                schedule(s, LOAD_CORE.with("depth", depth).append("load-count", loadCount + 1));
×
584
            }
585

586
        }
×
587

588
        // At the end of this step, we have added new endorsement
589
        // links to yet-to-retrieve agent introductions:
590
        // ------------------------------------------------------------
591
        //
592
        //         o      ----endorses----> [intro]
593
        //    --> /#\  /o\___            (to-retrieve)
594
        //        / \  \_/^^^
595
        //         (visited)
596
        //
597
        //    ========[X] trust path
598
        //
599
        // ------------------------------------------------------------
600
        // Only one endorsement is shown here, but there are typically
601
        // several.
602

603
    },
604

605
    FINISH_ITERATION {
33✔
606
        public void run(ClientSession s, Document taskDoc) {
607

608
            int depth = taskDoc.getInteger("depth");
×
609
            int loadCount = taskDoc.getInteger("load-count");
×
610

611
            if (loadCount == 0) {
×
612
                logger.info("No new cores loaded; finishing iteration");
×
613
                schedule(s, CALCULATE_TRUST_SCORES);
×
614
            } else if (depth == MAX_TRUST_PATH_DEPTH) {
×
615
                logger.info("Maximum depth reached: {}", depth);
×
616
                schedule(s, CALCULATE_TRUST_SCORES);
×
617
            } else {
618
                logger.info("Progressing iteration at depth {}", depth + 1);
×
619
                schedule(s, LOAD_DECLARATIONS.with("depth", depth + 1));
×
620
            }
621

622
        }
×
623

624
    },
625

626
    CALCULATE_TRUST_SCORES {
33✔
627

628
        // DB read from: accounts, trustPaths
629
        // DB write to:  accounts
630

631
        public void run(ClientSession s, Document taskDoc) {
632
            logger.info("Running CALCULATE_TRUST_SCORES task");
×
633

634
            while (true) {
635
                Document d = getOne(s, "accounts_loading", new Document("status", expanded.getValue()));
×
636
                if (d == null) {
×
637
                    break;
×
638
                }
639

640
                double ratio = 0.0;
×
641
                Map<String, Boolean> seenPathElements = new HashMap<>();
×
642
                int pathCount = 0;
×
643
                try (MongoCursor<Document> trustPaths = collection("trustPaths_loading").find(s,
×
644
                        new Document("agent", d.get("agent").toString()).append("pubkey", d.get("pubkey").toString())
×
645
                ).sort(orderBy(ascending("depth"), descending("ratio"), ascending("sorthash"))).cursor()) {
×
646
                    while (trustPaths.hasNext()) {
×
647
                        Document trustPath = trustPaths.next();
×
648
                        ratio += trustPath.getDouble("ratio");
×
649
                        boolean independentPath = true;
×
650
                        String[] pathElements = trustPath.getString("_id").split(" ");
×
651
                        // Iterate over path elements, ignoring first (root) and last (this agent/pubkey):
652
                        for (int i = 1; i < pathElements.length - 1; i++) {
×
653
                            String p = pathElements[i];
×
654
                            if (seenPathElements.containsKey(p)) {
×
655
                                independentPath = false;
×
656
                                break;
×
657
                            }
658
                            seenPathElements.put(p, true);
×
659
                        }
660
                        if (independentPath) {
×
661
                            pathCount += 1;
×
662
                        }
663
                    }
×
664
                }
665
                double rawQuota = GLOBAL_QUOTA * ratio;
×
666
                int quota = (int) rawQuota;
×
667
                if (rawQuota < MIN_USER_QUOTA) {
×
668
                    quota = MIN_USER_QUOTA;
×
669
                } else if (rawQuota > MAX_USER_QUOTA) {
×
670
                    quota = MAX_USER_QUOTA;
×
671
                }
672
                set(s, "accounts_loading",
×
673
                        d.append("status", processed.getValue())
×
674
                                .append("ratio", ratio)
×
675
                                .append("pathCount", pathCount)
×
676
                                .append("quota", quota)
×
677
                );
678
            }
×
679
            logger.info("CALCULATE_TRUST_SCORES complete");
×
680
            schedule(s, AGGREGATE_AGENTS);
×
681
        }
×
682

683
    },
684

685
    AGGREGATE_AGENTS {
33✔
686

687
        // DB read from: accounts, agents
688
        // DB write to:  accounts, agents
689

690
        public void run(ClientSession s, Document taskDoc) {
691
            logger.info("Running AGGREGATE_AGENTS task");
×
692

693
            while (true) {
694
                Document a = getOne(s, "accounts_loading", new Document("status", processed.getValue()));
×
695
                if (a == null) {
×
696
                    break;
×
697
                }
698

699
                Document agentId = new Document("agent", a.get("agent").toString()).append("status", processed.getValue());
×
700
                int count = 0;
×
701
                int pathCountSum = 0;
×
702
                double totalRatio = 0.0d;
×
703
                // Canonical-name resolution across the agent's approved keys: pick the
704
                // row with MAX(ratio); ties broken on lex-min name for determinism.
705
                // Per-(agent, pubkey) name was already chosen by LOAD_ENDORSEMENTS as
706
                // "latest declaring intro wins"; this layer just folds across keys.
707
                String chosenName = null;
×
708
                double chosenRatio = Double.NEGATIVE_INFINITY;
×
709
                try (MongoCursor<Document> agentAccounts = collection("accounts_loading").find(s, agentId).cursor()) {
×
710
                    while (agentAccounts.hasNext()) {
×
711
                        Document d = agentAccounts.next();
×
712
                        count++;
×
713
                        pathCountSum += d.getInteger("pathCount");
×
714
                        double r = d.getDouble("ratio");
×
715
                        totalRatio += r;
×
716
                        String n = d.getString("name");
×
717
                        if (n != null && (r > chosenRatio
×
718
                                          || (r == chosenRatio && (chosenName == null || n.compareTo(chosenName) < 0)))) {
×
719
                            chosenName = n;
×
720
                            chosenRatio = r;
×
721
                        }
722
                    }
×
723
                }
724
                collection("accounts_loading").updateMany(s, agentId, new Document("$set",
×
725
                        new DbEntryWrapper(aggregated).getDocument()));
×
726
                insert(s, "agents_loading",
×
727
                        agentId.append("accountCount", count)
×
728
                                .append("avgPathCount", (double) pathCountSum / count)
×
729
                                .append("totalRatio", totalRatio)
×
730
                                .append("name", chosenName)
×
731
                );
732
            }
×
733
            logger.info("AGGREGATE_AGENTS complete");
×
734
            schedule(s, ASSIGN_PUBKEYS);
×
735

736
        }
×
737

738
    },
739

740
    ASSIGN_PUBKEYS {
33✔
741

742
        // DB read from: accounts
743
        // DB write to:  accounts
744

745
        public void run(ClientSession s, Document taskDoc) {
746
            logger.info("Running ASSIGN_PUBKEYS task");
×
747
            int approvedCount = 0;
×
748
            int contestedCount = 0;
×
749

750
            while (true) {
751
                Document a = getOne(s, "accounts_loading", new DbEntryWrapper(aggregated).getDocument());
×
752
                if (a == null) {
×
753
                    break;
×
754
                }
755

756
                Document pubkeyId = new Document("pubkey", a.get("pubkey").toString());
×
757
                if (collection("accounts_loading").countDocuments(s, pubkeyId) == 1) {
×
758
                    collection("accounts_loading").updateMany(s, pubkeyId,
×
759
                            new Document("$set", new DbEntryWrapper(approved).getDocument()));
×
760
                    approvedCount++;
×
761
                } else {
762
                    // TODO At the moment all get marked as 'contested'; implement more nuanced algorithm
763
                    logger.debug("Pubkey {} is claimed by multiple accounts; marking contested", pubkeyId.getString("pubkey"));
×
764
                    collection("accounts_loading").updateMany(s, pubkeyId, new Document("$set",
×
765
                            new DbEntryWrapper(contested).getDocument()));
×
766
                    contestedCount++;
×
767
                }
768
            }
×
769
            logger.info("ASSIGN_PUBKEYS complete: {} approved, {} contested", approvedCount, contestedCount);
×
770
            schedule(s, DETERMINE_UPDATES);
×
771

772
        }
×
773

774
    },
775

776
    DETERMINE_UPDATES {
33✔
777

778
        // DB read from: accounts
779
        // DB write to:  accounts
780

781
        public void run(ClientSession s, Document taskDoc) {
782
            logger.info("Running DETERMINE_UPDATES task");
×
783

784
            // TODO Handle contested accounts properly:
785
            for (Document d : collection("accounts_loading").find(
×
786
                    new DbEntryWrapper(approved).getDocument())) {
×
787
                // TODO Consider quota too:
788
                Document accountId = new Document("agent", d.get("agent").toString()).append("pubkey", d.get("pubkey").toString());
×
789
                if (collection(Collection.ACCOUNTS.toString()) == null || !has(s, Collection.ACCOUNTS.toString(),
×
790
                        accountId.append("status", loaded.getValue()))) {
×
791
                    set(s, "accounts_loading", d.append("status", toLoad.getValue()));
×
792
                } else {
793
                    set(s, "accounts_loading", d.append("status", loaded.getValue()));
×
794
                }
795
            }
×
796
            logger.info("DETERMINE_UPDATES complete");
×
797
            schedule(s, FINALIZE_TRUST_STATE);
×
798

799
        }
×
800

801
    },
802

803
    FINALIZE_TRUST_STATE {
33✔
804
        // We do this is a separate task/transaction, because if we do it at the beginning of RELEASE_DATA, that task hangs and cannot
805
        // properly re-run (as some renaming outside of transactions will have taken place).
806
        public void run(ClientSession s, Document taskDoc) {
807
            logger.info("Running FINALIZE_TRUST_STATE task");
×
808
            String newTrustStateHash = RegistryDB.calculateTrustStateHash(s);
×
809
            String previousTrustStateHash = (String) getValue(s, Collection.SERVER_INFO.toString(), "trustStateHash");  // may be null
×
810
            logger.info("Computed new trust state hash {} (previous: {})", newTrustStateHash, previousTrustStateHash);
×
811
            setValue(s, Collection.SERVER_INFO.toString(), "lastTrustStateUpdate", ZonedDateTime.now().toString());
×
812

813
            schedule(s, RELEASE_DATA.with("newTrustStateHash", newTrustStateHash).append("previousTrustStateHash", previousTrustStateHash));
×
814
        }
×
815

816
    },
817

818
    RELEASE_DATA {
33✔
819

820
        private static final int TRUST_STATE_SNAPSHOT_RETENTION = 100;
821

822
        public void run(ClientSession s, Document taskDoc) {
823
            ServerStatus status = getServerStatus(s);
×
824
            logger.info("Running RELEASE_DATA task (current status: {})", status);
×
825

826
            String newTrustStateHash = taskDoc.get("newTrustStateHash").toString();
×
827
            String previousTrustStateHash = taskDoc.getString("previousTrustStateHash");  // may be null
×
828

829
            // Renaming collections is run outside of a transaction, but is idempotent operation, so can safely be retried if task fails:
830
            logger.debug("Renaming loading collections into live collections");
×
831
            rename("accounts_loading", Collection.ACCOUNTS.toString());
×
832
            rename("trustPaths_loading", "trustPaths");
×
833
            rename("agents_loading", Collection.AGENTS.toString());
×
834
            rename("endorsements_loading", "endorsements");
×
835

836
            if (previousTrustStateHash == null || !previousTrustStateHash.equals(newTrustStateHash)) {
×
837
                logger.info("Trust state changed ({} -> {}); recording new state and snapshot", previousTrustStateHash, newTrustStateHash);
×
838
                increaseStateCounter(s);
×
839
                setValue(s, Collection.SERVER_INFO.toString(), "trustStateHash", newTrustStateHash);
×
840
                Object trustStateCounter = getValue(s, Collection.SERVER_INFO.toString(), "trustStateCounter");
×
841
                insert(s, "debug_trustPaths", new Document()
×
842
                        .append("trustStateTxt", DebugPage.getTrustPathsTxt(s))
×
843
                        .append("trustStateHash", newTrustStateHash)
×
844
                        .append("trustStateCounter", trustStateCounter)
×
845
                );
846

847
                // Structured hash-keyed snapshot for consumer mirroring (#107).
848
                // Reads the accounts collection just renamed from accounts_loading above (:697).
849
                List<Document> snapshotAccounts = new ArrayList<>();
×
850
                for (Document a : collection(Collection.ACCOUNTS.toString()).find(s)) {
×
851
                    String pubkey = a.getString("pubkey");
×
852
                    if ("$".equals(pubkey)) {
×
853
                        continue;
×
854
                    }
855
                    // 'toLoad' is an internal-only staging status (not yet servable). Excluding it here
856
                    // — together with the matching exclusion in calculateTrustStateHash — keeps transient
857
                    // 'toLoad' accounts out of the published snapshot, so an account enters the public
858
                    // trust state exactly when it becomes loaded, never stuck at 'toLoad' (issue #119).
859
                    if (toLoad.getValue().equals(a.getString("status"))) {
×
860
                        continue;
×
861
                    }
862
                    snapshotAccounts.add(new Document()
×
863
                            .append("pubkey", pubkey)
×
864
                            .append("agent", a.getString("agent"))
×
865
                            .append("name", a.getString("name"))
×
866
                            .append("nameCreatedAt", a.get("nameCreatedAt"))
×
867
                            .append("introNanopub", a.getString("introNanopub"))
×
868
                            .append("status", a.getString("status"))
×
869
                            .append("depth", a.get("depth"))
×
870
                            .append("pathCount", a.get("pathCount"))
×
871
                            .append("ratio", a.get("ratio"))
×
872
                            .append("quota", a.get("quota")));
×
873
                }
×
874
                Document snapshot = new Document()
×
875
                        .append("_id", newTrustStateHash)
×
876
                        .append("trustStateCounter", trustStateCounter)
×
877
                        .append("createdAt", ZonedDateTime.now().toString())
×
878
                        .append("accounts", snapshotAccounts);
×
879
                collection(Collection.TRUST_STATE_SNAPSHOTS.toString()).replaceOne(
×
880
                        s,
881
                        new Document("_id", newTrustStateHash),
882
                        snapshot,
883
                        new ReplaceOptions().upsert(true));
×
884
                logger.debug("Saved trust state snapshot {} with {} account(s)", newTrustStateHash, snapshotAccounts.size());
×
885

886
                // Prune beyond retention: collect _ids of snapshots past the Nth most recent, delete them.
887
                // trustStateCounter is monotonically increasing (see increaseStateCounter above), so ordering is well-defined.
888
                List<Object> toPrune = new ArrayList<>();
×
889
                try (MongoCursor<Document> stale = collection(Collection.TRUST_STATE_SNAPSHOTS.toString())
×
890
                        .find(s)
×
891
                        .sort(descending("trustStateCounter"))
×
892
                        .skip(TRUST_STATE_SNAPSHOT_RETENTION)
×
893
                        .projection(new Document("_id", 1))
×
894
                        .cursor()) {
×
895
                    while (stale.hasNext()) {
×
896
                        toPrune.add(stale.next().get("_id"));
×
897
                    }
898
                }
899
                if (!toPrune.isEmpty()) {
×
900
                    logger.info("Pruning {} trust state snapshot(s) beyond retention limit of {}", toPrune.size(), TRUST_STATE_SNAPSHOT_RETENTION);
×
901
                    collection(Collection.TRUST_STATE_SNAPSHOTS.toString()).deleteMany(
×
902
                            s, new Document("_id", new Document("$in", toPrune)));
903
                }
904
            } else {
×
905
                logger.info("Trust state unchanged ({}); skipping snapshot and pruning", newTrustStateHash);
×
906
            }
907

908
            if (status == coreLoading) {
×
909
                logger.info("Server status transitioning coreLoading -> coreReady");
×
910
                setServerStatus(s, coreReady);
×
911
            } else {
912
                logger.info("Server status transitioning {} -> ready", status);
×
913
                setServerStatus(s, ready);
×
914
            }
915

916
            // Run update 10min after this cycle finishes (the delay is measured from the end of the
917
            // recompute, since this is the last task in the chain):
918
            logger.debug("RELEASE_DATA complete; scheduling next UPDATE in 10min");
×
919
            schedule(s, UPDATE.withDelay(10 * 60 * 1000));
×
920
        }
×
921

922
    },
923

924
    UPDATE {
33✔
925
        public void run(ClientSession s, Document taskDoc) {
926
            ServerStatus status = getServerStatus(s);
×
927
            if (status == ready || status == coreReady) {
×
928
                logger.info("Server status {} eligible for update; transitioning to updating and scheduling INIT_COLLECTIONS", status);
×
929
                setServerStatus(s, updating);
×
930
                schedule(s, INIT_COLLECTIONS);
×
931
            } else {
932
                logger.info("Postponing update; currently in status {}", status);
×
933
                schedule(s, UPDATE.withDelay(10 * 60 * 1000));
×
934
            }
935

936
        }
×
937

938
    },
939

940
    LOAD_FULL {
33✔
941
        public void run(ClientSession s, Document taskDoc) {
942
            logger.debug("LOAD_FULL invoked; taskDoc={}", taskDoc);
12✔
943

944
            if ("false".equals(System.getenv("REGISTRY_PERFORM_FULL_LOAD"))) {
15!
945
                logger.info("REGISTRY_PERFORM_FULL_LOAD=false; skipping full load");
×
946
                return;
×
947
            }
948

949
            ServerStatus status = getServerStatus(s);
9✔
950
            logger.debug("Server status check for LOAD_FULL: status={}", status);
12✔
951
            if (status != coreReady && status != ready && status != updating) {
27!
952
                long delay = 1000;
6✔
953
                logger.info("Server status={} is not eligible for full load (expected coreReady/ready/updating); deferring and retrying in {}ms", status, delay);
18✔
954
                schedule(s, LOAD_FULL.withDelay(delay));
15✔
955
                return;
3✔
956
            }
957

958
            Document a = getOne(s, Collection.ACCOUNTS.toString(), new DbEntryWrapper(toLoad).getDocument());
×
959
            if (a == null) {
×
960
                logger.info("No accounts left with status={}; full load pass complete", toLoad);
×
961
                if (status == coreReady) {
×
962
                    logger.info("Server status transitioning coreReady -> ready; full load finished");
×
963
                    setServerStatus(s, ready);
×
964
                }
965
                long delay = 100;
×
966
                logger.info("Scheduling optional loading checks (RUN_OPTIONAL_LOAD) in {}ms", delay);
×
967
                schedule(s, RUN_OPTIONAL_LOAD.withDelay(delay));
×
968
            } else {
×
969
                final String ph = a.getString("pubkey");
×
970
                boolean quotaReached = false;
×
971
                if (!ph.equals("$")) {
×
972
                    if (!AgentFilter.isAllowed(s, ph)) {
×
973
                        logger.info("Pubkey {} is not covered by the agent filter; marking account as skipped", ph);
×
974
                        set(s, Collection.ACCOUNTS.toString(), a.append("status", skipped.getValue()));
×
975
                        schedule(s, LOAD_FULL.withDelay(100));
×
976
                        return;
×
977
                    }
978
                    if (AgentFilter.isOverQuota(s, ph)) {
×
979
                        logger.info("Pubkey {} is already over quota; skipping load, marking account as capped", ph);
×
980
                        quotaReached = true;
×
981
                    } else {
982
                        long startTime = System.nanoTime();
×
983
                        AtomicLong totalLoaded = new AtomicLong(0);
×
984

985
                        // Load per covered type (or "$" if no restriction) with checksum skip-ahead
986
                        for (String typeHash : getLoadTypeHashes(s, ph)) {
×
987
                            logger.debug("Pubkey {}: starting load for typeHash={}", ph, typeHash);
×
988
                            String checksums = buildChecksumFallbacks(s, ph, typeHash);
×
989
                            logger.debug("Pubkey {}, typeHash={}: checksum fallbacks={}", ph, typeHash, checksums);
×
990
                            try (var stream = NanopubLoader.retrieveNanopubsFromPeers(typeHash, ph, checksums)) {
×
991
                                NanopubLoader.loadStreamInParallel(stream, np -> {
×
992
                                    if (!CoverageFilter.isCovered(np)) {
×
993
                                        logger.debug("Pubkey {}: nanopub {} not covered; skipping", ph, np);
×
994
                                        return;
×
995
                                    }
996
                                    try (ClientSession ws = RegistryDB.getClient().startSession()) {
×
997
                                        if (!AgentFilter.isOverQuota(ws, ph)) {
×
998
                                            loadNanopub(ws, np, ph, "$");
×
999
                                            totalLoaded.incrementAndGet();
×
1000
                                        } else {
1001
                                            logger.debug("Pubkey {} hit quota mid-stream; skipping nanopub {}", ph, np);
×
1002
                                        }
1003
                                    }
1004
                                });
×
1005
                            }
1006
                            logger.debug("Pubkey {}: finished load for typeHash={}", ph, typeHash);
×
1007
                        }
×
1008

1009
                        double timeSeconds = (System.nanoTime() - startTime) * 1e-9;
×
1010
                        logger.info("Pubkey {}: loaded {} nanopubs in {}s ({} np/s)",
×
1011
                                ph, totalLoaded.get(), String.format("%.2f", timeSeconds),
×
1012
                                String.format("%.2f", totalLoaded.get() / timeSeconds));
×
1013

1014
                        if (AgentFilter.isOverQuota(s, ph)) {
×
1015
                            logger.info("Pubkey {} reached quota during this load; marking account as capped", ph);
×
1016
                            quotaReached = true;
×
1017
                        }
1018
                    }
×
1019
                } else {
1020
                    logger.debug("Account pubkey is '$' (unrestricted); no per-pubkey quota/filter checks applied");
×
1021
                }
1022

1023
                Document l = getOne(s, "lists", new Document().append("pubkey", ph).append("type", "$"));
×
1024
                if (l != null) {
×
1025
                    logger.debug("Pubkey {}: marking matching list entry as loaded", ph);
×
1026
                    set(s, "lists", l.append("status", loaded.getValue()));
×
1027
                }
1028
                EntryStatus accountStatus = quotaReached ? capped : loaded;
×
1029
                int effectiveQuota = AgentFilter.getQuota(s, ph);
×
1030
                if (effectiveQuota >= 0) {
×
1031
                    logger.debug("Pubkey {}: recording effective quota={}", ph, effectiveQuota);
×
1032
                    a.append("quota", effectiveQuota);
×
1033
                }
1034
                logger.info("Pubkey {}: account load complete, status={}", ph, accountStatus);
×
1035
                set(s, Collection.ACCOUNTS.toString(), a.append("status", accountStatus.getValue()));
×
1036

1037
                schedule(s, LOAD_FULL.withDelay(100));
×
1038
            }
1039
        }
×
1040

1041
        @Override
1042
        public boolean runAsTransaction() {
1043
            // TODO Make this a transaction once we connect to other Nanopub Registry instances:
1044
            return false;
×
1045
        }
1046

1047
    },
1048

1049
    RUN_OPTIONAL_LOAD {
33✔
1050

1051
        private static final int BATCH_SIZE = Integer.parseInt(
15✔
1052
                Utils.getEnv("REGISTRY_OPTIONAL_LOAD_BATCH_SIZE", "100"));
3✔
1053

1054
        public void run(ClientSession s, Document taskDoc) {
1055
            if ("false".equals(System.getenv("REGISTRY_ENABLE_OPTIONAL_LOAD"))) {
×
1056
                schedule(s, CHECK_NEW.withDelay(500));
×
1057
                return;
×
1058
            }
1059

1060
            AtomicLong totalLoaded = new AtomicLong(0);
×
1061

1062
            // Phase 1: Process encountered intro lists (core loading)
1063
            while (totalLoaded.get() < BATCH_SIZE) {
×
1064
                Document di = getOne(s, "lists", new Document("type", INTRO_TYPE_HASH).append("status", encountered.getValue()));
×
1065
                if (di == null) {
×
1066
                    break;
×
1067
                }
1068

1069
                final String pubkeyHash = di.getString("pubkey");
×
1070
                Validate.notNull(pubkeyHash);
×
1071
                logger.info("Optional core loading: {}", pubkeyHash);
×
1072

1073
                String introChecksums = buildChecksumFallbacks(s, pubkeyHash, INTRO_TYPE_HASH);
×
1074
                try (var stream = NanopubLoader.retrieveNanopubsFromPeers(INTRO_TYPE_HASH, pubkeyHash, introChecksums)) {
×
1075
                    NanopubLoader.loadStreamInParallel(stream, np -> {
×
1076
                        try (ClientSession ws = RegistryDB.getClient().startSession()) {
×
1077
                            loadNanopub(ws, np, pubkeyHash, INTRO_TYPE);
×
1078
                            totalLoaded.incrementAndGet();
×
1079
                        }
1080
                    });
×
1081
                }
1082
                set(s, "lists", di.append("status", loaded.getValue()));
×
1083

1084
                String endorseChecksums = buildChecksumFallbacks(s, pubkeyHash, ENDORSE_TYPE_HASH);
×
1085
                try (var stream = NanopubLoader.retrieveNanopubsFromPeers(ENDORSE_TYPE_HASH, pubkeyHash, endorseChecksums)) {
×
1086
                    NanopubLoader.loadStreamInParallel(stream, np -> {
×
1087
                        try (ClientSession ws = RegistryDB.getClient().startSession()) {
×
1088
                            loadNanopub(ws, np, pubkeyHash, ENDORSE_TYPE);
×
1089
                            totalLoaded.incrementAndGet();
×
1090
                        }
1091
                    });
×
1092
                }
1093

1094
                Document de = new Document("pubkey", pubkeyHash).append("type", ENDORSE_TYPE_HASH);
×
1095
                if (has(s, "lists", de)) {
×
1096
                    set(s, "lists", de.append("status", loaded.getValue()));
×
1097
                } else {
1098
                    insert(s, "lists", de.append("status", loaded.getValue()));
×
1099
                }
1100

1101
                Document df = new Document("pubkey", pubkeyHash).append("type", "$");
×
1102
                if (!has(s, "lists", df)) {
×
1103
                    insert(s, "lists", df.append("status", encountered.getValue()));
×
1104
                }
1105
            }
×
1106

1107
            // Phase 2: Process encountered full lists (if budget remains)
1108
            while (totalLoaded.get() < BATCH_SIZE) {
×
1109
                Document df = getOne(s, "lists", new Document("type", "$").append("status", encountered.getValue()));
×
1110
                if (df == null) {
×
1111
                    break;
×
1112
                }
1113

1114
                final String pubkeyHash = df.getString("pubkey");
×
1115
                logger.info("Optional full loading: {}", pubkeyHash);
×
1116

1117
                // Load per covered type (or "$" if no restriction) with checksum skip-ahead
1118
                for (String typeHash : getLoadTypeHashes(s, pubkeyHash)) {
×
1119
                    String checksums = buildChecksumFallbacks(s, pubkeyHash, typeHash);
×
1120
                    try (var stream = NanopubLoader.retrieveNanopubsFromPeers(typeHash, pubkeyHash, checksums)) {
×
1121
                        NanopubLoader.loadStreamInParallel(stream, np -> {
×
1122
                            if (!CoverageFilter.isCovered(np)) {
×
1123
                                return;
×
1124
                            }
1125
                            try (ClientSession ws = RegistryDB.getClient().startSession()) {
×
1126
                                loadNanopub(ws, np, pubkeyHash, "$");
×
1127
                                totalLoaded.incrementAndGet();
×
1128
                            }
1129
                        });
×
1130
                    }
1131
                }
×
1132

1133
                set(s, "lists", df.append("status", loaded.getValue()));
×
1134

1135
                // Backfill nanopubs stored locally during the transitional period (i.e. before
1136
                // the $ list was loaded). Such nanopubs were stored in the nanopubs collection by
1137
                // simpleLoad() but never added to listEntries; add them to the $ list now.
1138
                logger.info("Backfilling locally stored nanopubs for pubkey: {}", pubkeyHash);
×
1139
                try (MongoCursor<Document> npCursor = collection(Collection.NANOPUBS.toString())
×
1140
                        .find(s, new Document("pubkey", pubkeyHash)).cursor()) {
×
1141
                    while (npCursor.hasNext()) {
×
1142
                        String fullId = npCursor.next().getString("fullId");
×
1143
                        if (fullId == null) {
×
1144
                            continue;
×
1145
                        }
1146
                        try (ClientSession ws = RegistryDB.getClient().startSession()) {
×
1147
                            Nanopub np = NanopubLoader.retrieveLocalNanopub(ws, fullId);
×
1148
                            if (np != null && CoverageFilter.isCovered(np)) {
×
1149
                                loadNanopub(ws, np, pubkeyHash, "$");
×
1150
                                totalLoaded.incrementAndGet();
×
1151
                            }
1152
                        } catch (Exception ex) {
×
1153
                            logger.info("Error backfilling nanopub {}: {}", fullId, ex.getMessage());
×
1154
                        }
×
1155
                    }
×
1156
                }
1157
            }
×
1158

1159
            if (totalLoaded.get() > 0) {
×
1160
                logger.info("Optional load batch completed: {} nanopubs across multiple pubkeys", totalLoaded.get());
×
1161
            }
1162

1163
            if (prioritizeAllPubkeys()) {
×
1164
                // Check if there are more pubkeys waiting to be processed
1165
                boolean moreWork = has(s, "lists", new Document("type", INTRO_TYPE_HASH).append("status", encountered.getValue()))
×
1166
                                   || has(s, "lists", new Document("type", "$").append("status", encountered.getValue()));
×
1167
                if (moreWork) {
×
1168
                    // Continue processing without a full CHECK_NEW cycle in between.
1169
                    // CHECK_NEW will run naturally once all encountered lists are processed.
1170
                    schedule(s, RUN_OPTIONAL_LOAD.withDelay(10));
×
1171
                } else {
1172
                    schedule(s, CHECK_NEW.withDelay(500));
×
1173
                }
1174
            } else {
×
1175
                // Throttled: yield to CHECK_NEW after each batch to prioritize approved pubkeys
1176
                schedule(s, CHECK_NEW.withDelay(500));
×
1177
            }
1178
        }
×
1179

1180
    },
1181

1182
    CHECK_NEW {
33✔
1183
        public void run(ClientSession s, Document taskDoc) {
1184
            logger.debug("Running CHECK_NEW task: checking peers and legacy source for new nanopubs");
×
1185

1186
            logger.debug("Checking peers for new nanopubs");
×
1187
            RegistryPeerConnector.checkPeers(s);
×
1188
            // Keep legacy connection during transition period:
1189
            logger.debug("Checking legacy connector for new nanopubs");
×
1190
            LegacyConnector.checkForNewNanopubs(s);
×
1191
            // TODO Somehow throttle the loading of such potentially non-approved nanopubs
1192

1193
            long delay = 100;
×
1194
            logger.debug("CHECK_NEW complete; scheduling LOAD_FULL with {}ms delay", delay);
×
1195
            schedule(s, LOAD_FULL.withDelay(delay));
×
1196
        }
×
1197

1198
        @Override
1199
        public boolean runAsTransaction() {
1200
            // Peer sync includes long-running streaming fetches that would exceed
1201
            // MongoDB's transaction timeout; each operation is individually safe.
1202
            return false;
×
1203
        }
1204

1205
    };
1206

1207
    private static final Logger logger = LoggerFactory.getLogger(Task.class);
9✔
1208

1209
    public abstract void run(ClientSession s, Document taskDoc) throws Exception;
1210

1211
    public boolean runAsTransaction() {
1212
        return true;
×
1213
    }
1214

1215
    Document asDocument() {
1216
        return withDelay(0L);
12✔
1217
    }
1218

1219
    private Document withDelay(long delay) {
1220
        // TODO Rename "not-before" to "notBefore" for consistency with other field names
1221
        return new Document()
15✔
1222
                .append("not-before", System.currentTimeMillis() + delay)
21✔
1223
                .append("action", name());
6✔
1224
    }
1225

1226
    private Document with(String key, Object value) {
1227
        return asDocument().append(key, value);
×
1228
    }
1229

1230
    private static boolean prioritizeAllPubkeys() {
1231
        return "true".equals(System.getenv("REGISTRY_PRIORITIZE_ALL_PUBKEYS"));
×
1232
    }
1233

1234
    /**
1235
     * Returns the type hashes to load for a given pubkey. When coverage is unrestricted,
1236
     * returns just "$" (all types in one request). When restricted, returns each covered
1237
     * type hash for per-type fetching with checksum skip-ahead.
1238
     * <p>
1239
     * TODO: Fetching "$" from peers with type restrictions will only return their covered
1240
     * types, not all types. To get full coverage, we'd need to fetch per-type from such peers.
1241
     * Additionally, checksum-based skip-ahead won't work correctly against such peers, because
1242
     * their "$" list has different checksums due to the differing type subset. This means full
1243
     * re-downloads on every cycle. Per-type fetching would solve both issues.
1244
     */
1245
    private static java.util.List<String> getLoadTypeHashes(ClientSession s, String pubkeyHash) {
1246
        if (CoverageFilter.coversAllTypes()) {
×
1247
            return java.util.List.of("$");
×
1248
        }
1249
        return java.util.List.copyOf(CoverageFilter.getCoveredTypeHashes());
×
1250
    }
1251

1252
    // TODO Move these to setting:
1253
    private static final int MAX_TRUST_PATH_DEPTH = 10;
1254
    private static final double MIN_TRUST_PATH_RATIO = 0.0000000001;
1255
    //private static final double MIN_TRUST_PATH_RATIO = 0.01; // For testing
1256
    private static final int GLOBAL_QUOTA = Integer.parseInt(
12✔
1257
            Utils.getEnv("REGISTRY_GLOBAL_QUOTA", "1000000000"));
3✔
1258
    private static final int MIN_USER_QUOTA = Integer.parseInt(
12✔
1259
            Utils.getEnv("REGISTRY_MIN_USER_QUOTA", "1000"));
3✔
1260
    private static final int MAX_USER_QUOTA = Integer.parseInt(
12✔
1261
            Utils.getEnv("REGISTRY_MAX_USER_QUOTA", "100000"));
3✔
1262

1263
    private static MongoCollection<Document> tasksCollection = collection(Collection.TASKS.toString());
15✔
1264

1265
    private static volatile String currentTaskName;
1266
    private static volatile long currentTaskStartTime;
1267

1268
    public static String getCurrentTaskName() {
1269
        return currentTaskName;
×
1270
    }
1271

1272
    public static long getCurrentTaskStartTime() {
1273
        return currentTaskStartTime;
×
1274
    }
1275

1276
    /**
1277
     * The super important base entry point!
1278
     */
1279
    static void runTasks() {
1280
        try (ClientSession s = RegistryDB.getClient().startSession()) {
×
1281
            if (!RegistryDB.isInitialized(s)) {
×
1282
                schedule(s, INIT_DB); // does not yet execute, only schedules
×
1283
            }
1284

1285
            logger.info("Task runner started");
×
1286
            while (true) {
1287
                FindIterable<Document> taskResult = tasksCollection.find(s).sort(ascending("not-before"));
×
1288
                Document taskDoc = taskResult.first();
×
1289
                long sleepTime = 10;
×
1290
                if (taskDoc != null && taskDoc.getLong("not-before") < System.currentTimeMillis()) {
×
1291
                    Task task = valueOf(taskDoc.getString("action"));
×
1292
                    Object taskId = taskDoc.getOrDefault("_id", null);
×
1293
                    logger.info("Picked task to run: {} (docId={})", task.name(), taskId);
×
1294

1295
                    if (task.runAsTransaction()) {
×
1296
                        try {
1297
                            s.startTransaction();
×
1298
                            logger.debug("Transaction started for task {}", task.name());
×
1299
                            runTask(task, taskDoc);
×
1300
                            s.commitTransaction();
×
1301
                            logger.info("Transaction committed for task {}", task.name());
×
1302
                        } catch (Exception ex) {
×
1303
                            logger.warn("Transactional task {} failed, aborting: {}", task.name(), ex.getMessage(), ex);
×
1304
                            abortTransaction(s, ex.getMessage());
×
1305
                            logger.info("Transaction aborted for task {}", task.name());
×
1306
                            sleepTime = 1000;
×
1307
                        } finally {
1308
                            cleanTransactionWithRetry(s);
×
1309
                        }
×
1310
                    } else {
1311
                        try {
1312
                            runTask(task, taskDoc);
×
1313
                        } catch (Exception ex) {
×
1314
                            logger.warn("Non-transactional task {} failed: {}", task.name(), ex.getMessage(), ex);
×
1315
                        }
×
1316
                    }
1317
                }
1318
                try {
1319
                    Thread.sleep(sleepTime);
×
1320
                } catch (InterruptedException ex) {
×
1321
                    // ignore
1322
                    logger.debug("Task runner sleep interrupted");
×
1323
                }
×
1324
            }
×
1325
        }
1326
    }
1327

1328
    static void runTask(Task task, Document taskDoc) throws Exception {
1329
        currentTaskName = task.name();
9✔
1330
        currentTaskStartTime = System.currentTimeMillis();
6✔
1331
        logger.info("Starting task {} with request: {}", currentTaskName, taskDoc);
15✔
1332

1333
        try (ClientSession s = RegistryDB.getClient().startSession()) {
9✔
1334
            task.run(s, taskDoc);
12✔
1335
            long duration = System.currentTimeMillis() - currentTaskStartTime;
12✔
1336
            tasksCollection.deleteOne(s, eq("_id", taskDoc.get("_id")));
27✔
1337
            logger.info("Completed and removed from queue task {} in {} ms", currentTaskName, duration);
18✔
1338
        } catch (Exception ex) {
×
1339
            long duration = System.currentTimeMillis() - currentTaskStartTime;
×
1340
            logger.error("Task {} failed after {} ms: {}", currentTaskName, duration, ex.getMessage(), ex);
×
1341
            throw ex;
×
1342
        } finally {
1343
            currentTaskName = null;
6✔
1344
        }
1345
    }
3✔
1346

1347
    public static void abortTransaction(ClientSession mongoSession, String message) {
1348
        boolean successful = false;
×
1349
        while (!successful) {
×
1350
            try {
1351
                if (mongoSession.hasActiveTransaction()) {
×
1352
                    logger.info("Attempting to abort transaction: {}", message);
×
1353
                    mongoSession.abortTransaction();
×
1354
                }
1355
                successful = true;
×
1356
                logger.debug("abortTransaction succeeded");
×
1357
            } catch (Exception ex) {
×
1358
                logger.warn("abortTransaction attempt failed: {}", ex.getMessage(), ex);
×
1359
                try {
1360
                    Thread.sleep(100);
×
1361
                } catch (InterruptedException ie) {
×
1362
                    logger.debug("abortTransaction sleep interrupted");
×
1363
                }
×
1364
            }
×
1365
        }
1366
    }
×
1367

1368
    public synchronized static void cleanTransactionWithRetry(ClientSession mongoSession) {
1369
        boolean successful = false;
×
1370
        while (!successful) {
×
1371
            try {
1372
                if (mongoSession.hasActiveTransaction()) {
×
1373
                    mongoSession.abortTransaction();
×
1374
                    logger.debug("abortTransaction during cleanup succeeded");
×
1375
                }
1376
                successful = true;
×
1377
                logger.debug("Transaction cleanup completed");
×
1378
            } catch (Exception ex) {
×
1379
                logger.warn("Transaction cleanup failed: {}", ex.getMessage(), ex);
×
1380
                try {
1381
                    Thread.sleep(100);
×
1382
                } catch (InterruptedException ie) {
×
1383
                    logger.debug("cleanTransactionWithRetry sleep interrupted");
×
1384
                }
×
1385
            }
×
1386
        }
1387
    }
×
1388

1389
    private static IntroNanopub getAgentIntro(ClientSession mongoSession, String nanopubId) {
1390
        IntroNanopub agentIntro = new IntroNanopub(NanopubLoader.retrieveNanopub(mongoSession, nanopubId));
×
1391
        if (agentIntro.getUser() == null) {
×
1392
            logger.debug("Nanopub {} is not a valid agent intro (no declared user); discarding", nanopubId);
×
1393
            return null;
×
1394
        }
1395
        loadNanopub(mongoSession, agentIntro.getNanopub());
×
1396
        return agentIntro;
×
1397
    }
1398

1399

1400
    private static void setServerStatus(ClientSession mongoSession, ServerStatus status) {
1401
        setValue(mongoSession, Collection.SERVER_INFO.toString(), "status", status.toString());
21✔
1402
    }
3✔
1403

1404
    private static ServerStatus getServerStatus(ClientSession mongoSession) {
1405
        Object status = getValue(mongoSession, Collection.SERVER_INFO.toString(), "status");
18✔
1406
        if (status == null) {
6!
1407
            throw new RuntimeException("Illegal DB state: serverInfo status unavailable");
×
1408
        }
1409
        return ServerStatus.valueOf(status.toString());
12✔
1410
    }
1411

1412
    private static void schedule(ClientSession mongoSession, Task task) {
1413
        schedule(mongoSession, task.asDocument());
12✔
1414
    }
3✔
1415

1416
    private static void schedule(ClientSession mongoSession, Document taskDoc) {
1417
        logger.info("Scheduling task: {}", taskDoc.getString("action"));
18✔
1418
        tasksCollection.insertOne(mongoSession, taskDoc);
12✔
1419
    }
3✔
1420

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