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

knowledgepixels / nanopub-registry / 28094324302

24 Jun 2026 11:10AM UTC coverage: 32.271% (+0.2%) from 32.089%
28094324302

Pull #116

github

web-flow
Merge 8d4b30b75 into b71f518fc
Pull Request #116: Enhance and standardize logging across multiple components

312 of 1094 branches covered (28.52%)

Branch coverage included in aggregate %.

1024 of 3046 relevant lines covered (33.62%)

5.62 hits per line

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

10.68
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
            setServerStatus(s, launching);
9✔
41

42
            increaseStateCounter(s);
6✔
43
            if (RegistryDB.isInitialized(s)) {
9!
44
                throw new RuntimeException("DB already initialized");
×
45
            }
46
            setValue(s, Collection.SERVER_INFO.toString(), "setupId", Math.abs(Utils.getRandom().nextLong()));
27✔
47
            setValue(s, Collection.SERVER_INFO.toString(), "testInstance", "true".equals(System.getenv("REGISTRY_TEST_INSTANCE")));
30✔
48
            schedule(s, LOAD_CONFIG);
9✔
49
        }
3✔
50

51
    },
52

53
    LOAD_CONFIG {
33✔
54
        public void run(ClientSession s, Document taskDoc) {
55
            if (getServerStatus(s) != launching) {
12!
56
                throw new IllegalTaskStatusException("Illegal status for this task: " + getServerStatus(s));
×
57
            }
58

59
            if (System.getenv("REGISTRY_COVERAGE_TYPES") != null) {
9!
60
                setValue(s, Collection.SERVER_INFO.toString(), "coverageTypes", System.getenv("REGISTRY_COVERAGE_TYPES"));
×
61
            }
62
            if (System.getenv("REGISTRY_COVERAGE_AGENTS") != null) {
9!
63
                setValue(s, Collection.SERVER_INFO.toString(), "coverageAgents", System.getenv("REGISTRY_COVERAGE_AGENTS"));
×
64
            }
65
            schedule(s, LOAD_SETTING);
9✔
66
        }
3✔
67

68
    },
69

70
    LOAD_SETTING {
33✔
71
        public void run(ClientSession s, Document taskDoc) throws Exception {
72
            if (getServerStatus(s) != launching) {
12!
73
                throw new IllegalTaskStatusException("Illegal status for this task: " + getServerStatus(s));
×
74
            }
75

76
            NanopubSetting settingNp = Utils.getSetting();
6✔
77
            String settingId = TrustyUriUtils.getArtifactCode(settingNp.getNanopub().getUri().stringValue());
18✔
78
            setValue(s, Collection.SETTING.toString(), "original", settingId);
18✔
79
            setValue(s, Collection.SETTING.toString(), "current", settingId);
18✔
80
            loadNanopub(s, settingNp.getNanopub());
15✔
81
            List<Document> bootstrapServices = new ArrayList<>();
12✔
82
            for (IRI i : settingNp.getBootstrapServices()) {
33✔
83
                bootstrapServices.add(new Document("_id", i.stringValue()));
27✔
84
            }
3✔
85
            // potentially currently hardcoded in the nanopub lib
86
            setValue(s, Collection.SETTING.toString(), "bootstrap-services", bootstrapServices);
18✔
87

88
            if (!"false".equals(System.getenv("REGISTRY_PERFORM_FULL_LOAD"))) {
15!
89
                schedule(s, LOAD_FULL);
9✔
90
            }
91

92
            setServerStatus(s, coreLoading);
9✔
93
            schedule(s, INIT_COLLECTIONS);
9✔
94
        }
3✔
95

96
    },
97

98
    INIT_COLLECTIONS {
33✔
99

100
        // DB read from:
101
        // DB write to:  trustPaths, endorsements, accounts
102
        // This state is periodically executed
103

104
        public void run(ClientSession s, Document taskDoc) throws Exception {
105
            if (getServerStatus(s) != coreLoading && getServerStatus(s) != updating) {
×
106
                throw new IllegalTaskStatusException("Illegal status for this task: " + getServerStatus(s));
×
107
            }
108

109
            IndexInitializer.initLoadingCollections(s);
×
110

111
            if ("false".equals(System.getenv("REGISTRY_ENABLE_TRUST_CALCULATION"))) {
×
112
                logger.info("Trust calculation disabled; skipping to FINALIZE_TRUST_STATE");
×
113
                for (Map.Entry<String, Integer> entry : AgentFilter.getExplicitPubkeys().entrySet()) {
×
114
                    String pubkeyHash = entry.getKey();
×
115
                    int quota = entry.getValue();
×
116
                    Document account = new Document("agent", "")
×
117
                            .append("pubkey", pubkeyHash)
×
118
                            .append("status", toLoad.getValue())
×
119
                            .append("depth", 0)
×
120
                            .append("quota", quota);
×
121
                    if (!has(s, "accounts_loading", new Document("pubkey", pubkeyHash))) {
×
122
                        insert(s, "accounts_loading", account);
×
123
                        logger.info("Seeded explicit pubkey as account: {}", pubkeyHash);
×
124
                    }
125
                }
×
126
                schedule(s, FINALIZE_TRUST_STATE);
×
127
                return;
×
128
            }
129

130
            // since this may take long, we start with postfix "_loading"
131
            // and only at completion it's changed to trustPath, endorsements, accounts
132
            insert(s, "trustPaths_loading",
×
133
                    new Document("_id", "$")
134
                            .append("sorthash", "")
×
135
                            .append("agent", "$")
×
136
                            .append("pubkey", "$")
×
137
                            .append("depth", 0)
×
138
                            .append("ratio", 1.0d)
×
139
                            .append("type", "extended")
×
140
            );
141

142
            NanopubIndex agentIndex = IndexUtils.castToIndex(NanopubLoader.retrieveNanopub(s, Utils.getSetting().getAgentIntroCollection().stringValue()));
×
143
            loadNanopub(s, agentIndex);
×
144
            for (IRI el : agentIndex.getElements()) {
×
145
                String declarationAc = TrustyUriUtils.getArtifactCode(el.stringValue());
×
146
                Validate.notNull(declarationAc);
×
147

148
                insert(s, "endorsements_loading",
×
149
                        new Document("agent", "$")
150
                                .append("pubkey", "$")
×
151
                                .append("endorsedNanopub", declarationAc)
×
152
                                .append("source", getValue(s, Collection.SETTING.toString(), "current").toString())
×
153
                                .append("status", toRetrieve.getValue())
×
154

155
                );
156

157
            }
×
158
            insert(s, "accounts_loading",
×
159
                    new Document("agent", "$")
160
                            .append("pubkey", "$")
×
161
                            .append("status", visited.getValue())
×
162
                            .append("depth", 0)
×
163
            );
164

165
            logger.info("Starting iteration at depth 0");
×
166
            schedule(s, LOAD_DECLARATIONS.with("depth", 1));
×
167
        }
×
168

169
        // At the end of this task, the base agent is initialized:
170
        // ------------------------------------------------------------
171
        //
172
        //              $$$$ ----endorses----> [intro]
173
        //              base                (to-retrieve)
174
        //              $$$$
175
        //            (visited)
176
        //
177
        //              [0] trust path
178
        //
179
        // ------------------------------------------------------------
180
        // Only one endorses-link to an introduction is shown here,
181
        // but there are typically several.
182

183
    },
184

185
    LOAD_DECLARATIONS {
33✔
186

187
        // In general, we have at this point accounts with
188
        // endorsement links to unvisited agent introductions:
189
        // ------------------------------------------------------------
190
        //
191
        //         o      ----endorses----> [intro]
192
        //    --> /#\  /o\___            (to-retrieve)
193
        //        / \  \_/^^^
194
        //         (visited)
195
        //
196
        //    ========[X] trust path
197
        //
198
        // ------------------------------------------------------------
199

200
        // DB read from: endorsements, trustEdges, accounts
201
        // DB write to:  endorsements, trustEdges, accounts
202

203
        public void run(ClientSession s, Document taskDoc) {
204

205
            int depth = taskDoc.getInteger("depth");
×
206

207
            while (true) {
208
                Document d = getOne(s, "endorsements_loading",
×
209
                        new DbEntryWrapper(toRetrieve).getDocument());
×
210
                if (d == null) {
×
211
                    break;
×
212
                }
213

214
                IntroNanopub agentIntro = getAgentIntro(s, d.getString("endorsedNanopub"));
×
215
                if (agentIntro != null) {
×
216
                    String agentId = agentIntro.getUser().stringValue();
×
217
                    // foaf:name + dct:created of the intro nanopub. Same name applies to every
218
                    // KeyDeclaration in the intro, so resolve once outside the inner loop.
219
                    String introName = Utils.extractIntroName(agentIntro);
×
220
                    Calendar introCreatedCal = SimpleTimestampPattern.getCreationTime(agentIntro.getNanopub());
×
221
                    Date introCreatedAt = (introCreatedCal == null) ? null : introCreatedCal.getTime();
×
222

223
                    for (KeyDeclaration kd : agentIntro.getKeyDeclarations()) {
×
224
                        String sourceAgent = d.getString("agent");
×
225
                        Validate.notNull(sourceAgent);
×
226
                        String sourcePubkey = d.getString("pubkey");
×
227
                        Validate.notNull(sourcePubkey);
×
228
                        String sourceAc = d.getString("source");
×
229
                        Validate.notNull(sourceAc);
×
230
                        String agentPubkey = Utils.getHash(kd.getPublicKeyString());
×
231
                        Validate.notNull(agentPubkey);
×
232
                        Document trustEdge = new Document("fromAgent", sourceAgent)
×
233
                                .append("fromPubkey", sourcePubkey)
×
234
                                .append("toAgent", agentId)
×
235
                                .append("toPubkey", agentPubkey)
×
236
                                .append("source", sourceAc);
×
237
                        if (!has(s, "trustEdges", trustEdge)) {
×
238
                            boolean invalidated = has(s, "invalidations", new Document("invalidatedNp", sourceAc).append("invalidatingPubkey", sourcePubkey));
×
239
                            insert(s, "trustEdges", trustEdge.append("invalidated", invalidated));
×
240
                        }
241

242
                        Document agent = new Document("agent", agentId).append("pubkey", agentPubkey);
×
243
                        Document existing = collection("accounts_loading").find(s, agent).first();
×
244
                        if (existing == null) {
×
245
                            insert(s, "accounts_loading", agent
×
246
                                    .append("status", seen.getValue())
×
247
                                    .append("depth", depth)
×
248
                                    .append("name", introName)
×
249
                                    .append("nameCreatedAt", introCreatedAt));
×
250
                        } else if (introName != null) {
×
251
                            // Per-(agent, pubkey) name policy: keep the name from the intro
252
                            // with the latest dct:created. First write wins when no current
253
                            // timestamp exists; otherwise compare and replace iff strictly newer.
254
                            Date currentCreatedAt = existing.getDate("nameCreatedAt");
×
255
                            if (currentCreatedAt == null
×
256
                                || (introCreatedAt != null && introCreatedAt.after(currentCreatedAt))) {
×
257
                                set(s, "accounts_loading", existing
×
258
                                        .append("name", introName)
×
259
                                        .append("nameCreatedAt", introCreatedAt));
×
260
                            }
261
                        }
262
                    }
×
263

264
                    set(s, "endorsements_loading", d.append("status", retrieved.getValue()));
×
265
                } else {
×
266
                    set(s, "endorsements_loading", d.append("status", discarded.getValue()));
×
267
                }
268
            }
×
269
            schedule(s, EXPAND_TRUST_PATHS.with("depth", depth));
×
270
        }
×
271

272
        // At the end of this step, the key declarations in the agent
273
        // introductions are loaded and the corresponding trust edges
274
        // established:
275
        // ------------------------------------------------------------
276
        //
277
        //        o      ----endorses----> [intro]
278
        //   --> /#\  /o\___                o
279
        //       / \  \_/^^^ ---trusts---> /#\  /o\___
280
        //        (visited)                / \  \_/^^^
281
        //                                   (seen)
282
        //
283
        //   ========[X] trust path
284
        //
285
        // ------------------------------------------------------------
286
        // Only one trust edge per introduction is shown here, but
287
        // there can be several.
288

289
    },
290

291
    EXPAND_TRUST_PATHS {
33✔
292

293
        // DB read from: accounts, trustPaths, trustEdges
294
        // DB write to:  accounts, trustPaths
295

296
        public void run(ClientSession s, Document taskDoc) {
297

298
            int depth = taskDoc.getInteger("depth");
×
299

300
            while (true) {
301
                Document d = getOne(s, "accounts_loading",
×
302
                        new Document("status", visited.getValue())
×
303
                                .append("depth", depth - 1)
×
304
                );
305
                if (d == null) {
×
306
                    break;
×
307
                }
308

309
                String agentId = d.getString("agent");
×
310
                Validate.notNull(agentId);
×
311
                String pubkeyHash = d.getString("pubkey");
×
312
                Validate.notNull(pubkeyHash);
×
313

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

318
                if (trustPath == null) {
×
319
                    // Check it again in next iteration:
320
                    set(s, "accounts_loading", d.append("depth", depth));
×
321
                } else {
322
                    // Only first matching trust path is considered
323

324
                    Map<String, Document> newPaths = new HashMap<>();
×
325
                    Map<String, Set<String>> pubkeySets = new HashMap<>();
×
326
                    String currentSetting = getValue(s, Collection.SETTING.toString(), "current").toString();
×
327

328
                    try (MongoCursor<Document> edgeCursor = get(s, "trustEdges",
×
329
                            new Document("fromAgent", agentId)
330
                                    .append("fromPubkey", pubkeyHash)
×
331
                                    .append("invalidated", false)
×
332
                    )) {
333
                        while (edgeCursor.hasNext()) {
×
334
                            Document e = edgeCursor.next();
×
335

336
                            String agent = e.getString("toAgent");
×
337
                            Validate.notNull(agent);
×
338
                            String pubkey = e.getString("toPubkey");
×
339
                            Validate.notNull(pubkey);
×
340
                            String pathId = trustPath.getString("_id") + " " + agent + "|" + pubkey;
×
341
                            newPaths.put(pathId,
×
342
                                    new Document("_id", pathId)
343
                                            .append("sorthash", Utils.getHash(currentSetting + " " + pathId))
×
344
                                            .append("agent", agent)
×
345
                                            .append("pubkey", pubkey)
×
346
                                            .append("depth", depth)
×
347
                                            .append("type", "extended")
×
348
                            );
349
                            if (!pubkeySets.containsKey(agent)) {
×
350
                                pubkeySets.put(agent, new HashSet<>());
×
351
                            }
352
                            pubkeySets.get(agent).add(pubkey);
×
353
                        }
×
354
                    }
355
                    for (String pathId : newPaths.keySet()) {
×
356
                        Document pd = newPaths.get(pathId);
×
357
                        // first divide by agents; then for each agent, divide by number of pubkeys:
358
                        double newRatio = (trustPath.getDouble("ratio") * 0.9) / pubkeySets.size() / pubkeySets.get(pd.getString("agent")).size();
×
359
                        insert(s, "trustPaths_loading", pd.append("ratio", newRatio));
×
360
                    }
×
361
                    // Retain only 10% of the ratio — the other 90% was distributed to children
362
                    double retainedRatio = trustPath.getDouble("ratio") * 0.1;
×
363
                    set(s, "trustPaths_loading", trustPath.append("type", "primary").append("ratio", retainedRatio));
×
364
                    set(s, "accounts_loading", d.append("status", expanded.getValue()));
×
365
                }
366
            }
×
367
            schedule(s, LOAD_CORE.with("depth", depth).append("load-count", 0));
×
368

369
        }
×
370

371
        // At the end of this step, trust paths are updated to include
372
        // the new accounts:
373
        // ------------------------------------------------------------
374
        //
375
        //         o      ----endorses----> [intro]
376
        //    --> /#\  /o\___                o
377
        //        / \  \_/^^^ ---trusts---> /#\  /o\___
378
        //        (expanded)                / \  \_/^^^
379
        //                                    (seen)
380
        //
381
        //    ========[X]=====================[X+1] trust path
382
        //
383
        // ------------------------------------------------------------
384
        // Only one trust path is shown here, but they branch out if
385
        // several trust edges are present.
386

387
    },
388

389
    LOAD_CORE {
33✔
390

391
        // From here on, we refocus on the head of the trust paths:
392
        // ------------------------------------------------------------
393
        //
394
        //         o
395
        //    --> /#\  /o\___
396
        //        / \  \_/^^^
397
        //          (seen)
398
        //
399
        //    ========[X] trust path
400
        //
401
        // ------------------------------------------------------------
402

403
        // DB read from: accounts, trustPaths, endorsements, lists
404
        // DB write to:  accounts, endorsements, lists
405

406
        public void run(ClientSession s, Document taskDoc) {
407

408
            int depth = taskDoc.getInteger("depth");
×
409
            int loadCount = taskDoc.getInteger("load-count");
×
410

411
            Document agentAccount = getOne(s, "accounts_loading",
×
412
                    new Document("depth", depth).append("status", seen.getValue()));
×
413
            final String agentId;
414
            final String pubkeyHash;
415
            final Document trustPath;
416
            if (agentAccount != null) {
×
417
                agentId = agentAccount.getString("agent");
×
418
                Validate.notNull(agentId);
×
419
                pubkeyHash = agentAccount.getString("pubkey");
×
420
                Validate.notNull(pubkeyHash);
×
421
                trustPath = getOne(s, "trustPaths_loading",
×
422
                        new Document("depth", depth)
×
423
                                .append("agent", agentId)
×
424
                                .append("pubkey", pubkeyHash)
×
425
                );
426
            } else {
427
                agentId = null;
×
428
                pubkeyHash = null;
×
429
                trustPath = null;
×
430
            }
431

432
            if (agentAccount == null) {
×
433
                schedule(s, FINISH_ITERATION.with("depth", depth).append("load-count", loadCount));
×
434
            } else if (trustPath == null) {
×
435
                // Account was seen but has no trust path at this depth; skip it
436
                set(s, "accounts_loading", agentAccount.append("status", skipped.getValue()));
×
437
                schedule(s, LOAD_CORE.with("depth", depth).append("load-count", loadCount));
×
438
            } else if (trustPath.getDouble("ratio") < MIN_TRUST_PATH_RATIO) {
×
439
                set(s, "accounts_loading", agentAccount.append("status", skipped.getValue()));
×
440
                Document d = new Document("pubkey", pubkeyHash).append("type", INTRO_TYPE_HASH);
×
441
                if (!has(s, "lists", d)) {
×
442
                    insert(s, "lists", d.append("status", encountered.getValue()));
×
443
                }
444
                schedule(s, LOAD_CORE.with("depth", depth).append("load-count", loadCount + 1));
×
445
            } else {
×
446
                // TODO check intro limit
447
                Document introList = new Document()
×
448
                        .append("pubkey", pubkeyHash)
×
449
                        .append("type", INTRO_TYPE_HASH)
×
450
                        .append("status", loading.getValue());
×
451
                if (!has(s, "lists", new Document("pubkey", pubkeyHash).append("type", INTRO_TYPE_HASH))) {
×
452
                    insert(s, "lists", introList);
×
453
                }
454

455
                // No checksum skip in LOAD_CORE: the endorsement extraction logic (below) needs to
456
                // see every nanopub to populate endorsements_loading, which is rebuilt from scratch each UPDATE.
457
                try (var stream = NanopubLoader.retrieveNanopubsFromPeers(INTRO_TYPE_HASH, pubkeyHash)) {
×
458
                    NanopubLoader.loadStreamInParallel(stream, np -> {
×
459
                        try (ClientSession ws = RegistryDB.getClient().startSession()) {
×
460
                            loadNanopub(ws, np, pubkeyHash, INTRO_TYPE);
×
461
                        }
462
                    });
×
463
                }
464

465
                set(s, "lists", introList.append("status", loaded.getValue()));
×
466

467
                // TODO check endorsement limit
468
                Document endorseList = new Document()
×
469
                        .append("pubkey", pubkeyHash)
×
470
                        .append("type", ENDORSE_TYPE_HASH)
×
471
                        .append("status", loading.getValue());
×
472
                if (!has(s, "lists", new Document("pubkey", pubkeyHash).append("type", ENDORSE_TYPE_HASH))) {
×
473
                    insert(s, "lists", endorseList);
×
474
                }
475

476
                try (var stream = NanopubLoader.retrieveNanopubsFromPeers(ENDORSE_TYPE_HASH, pubkeyHash)) {
×
477
                    stream.forEach(m -> {
×
478
                        if (!m.isSuccess()) {
×
479
                            throw new AbortingTaskException("Failed to download nanopub; aborting task...");
×
480
                        }
481
                        Nanopub nanopub = m.getNanopub();
×
482
                        loadNanopub(s, nanopub, pubkeyHash, ENDORSE_TYPE);
×
483
                        String sourceNpId = TrustyUriUtils.getArtifactCode(nanopub.getUri().stringValue());
×
484
                        Validate.notNull(sourceNpId);
×
485
                        for (Statement st : nanopub.getAssertion()) {
×
486
                            if (!st.getPredicate().equals(Utils.APPROVES_OF)) {
×
487
                                continue;
×
488
                            }
489
                            if (!(st.getObject() instanceof IRI)) {
×
490
                                continue;
×
491
                            }
492
                            if (!agentId.equals(st.getSubject().stringValue())) {
×
493
                                continue;
×
494
                            }
495
                            String objStr = st.getObject().stringValue();
×
496
                            if (!TrustyUriUtils.isPotentialTrustyUri(objStr)) {
×
497
                                continue;
×
498
                            }
499
                            String endorsedNpId = TrustyUriUtils.getArtifactCode(objStr);
×
500
                            Validate.notNull(endorsedNpId);
×
501
                            Document endorsement = new Document("agent", agentId)
×
502
                                    .append("pubkey", pubkeyHash)
×
503
                                    .append("endorsedNanopub", endorsedNpId)
×
504
                                    .append("source", sourceNpId);
×
505
                            if (!has(s, "endorsements_loading", endorsement)) {
×
506
                                insert(s, "endorsements_loading",
×
507
                                        endorsement.append("status", toRetrieve.getValue()));
×
508
                            }
509
                        }
×
510
                    });
×
511
                }
512

513
                set(s, "lists", endorseList.append("status", loaded.getValue()));
×
514

515
                Document df = new Document("pubkey", pubkeyHash).append("type", "$");
×
516
                if (!has(s, "lists", df)) {
×
517
                    insert(s, "lists",
×
518
                            df.append("status", encountered.getValue()));
×
519
                }
520

521
                set(s, "accounts_loading", agentAccount.append("status", visited.getValue()));
×
522

523
                schedule(s, LOAD_CORE.with("depth", depth).append("load-count", loadCount + 1));
×
524
            }
525

526
        }
×
527

528
        // At the end of this step, we have added new endorsement
529
        // links to yet-to-retrieve agent introductions:
530
        // ------------------------------------------------------------
531
        //
532
        //         o      ----endorses----> [intro]
533
        //    --> /#\  /o\___            (to-retrieve)
534
        //        / \  \_/^^^
535
        //         (visited)
536
        //
537
        //    ========[X] trust path
538
        //
539
        // ------------------------------------------------------------
540
        // Only one endorsement is shown here, but there are typically
541
        // several.
542

543
    },
544

545
    FINISH_ITERATION {
33✔
546
        public void run(ClientSession s, Document taskDoc) {
547

548
            int depth = taskDoc.getInteger("depth");
×
549
            int loadCount = taskDoc.getInteger("load-count");
×
550

551
            if (loadCount == 0) {
×
552
                logger.info("No new cores loaded; finishing iteration");
×
553
                schedule(s, CALCULATE_TRUST_SCORES);
×
554
            } else if (depth == MAX_TRUST_PATH_DEPTH) {
×
555
                logger.info("Maximum depth reached: {}", depth);
×
556
                schedule(s, CALCULATE_TRUST_SCORES);
×
557
            } else {
558
                logger.info("Progressing iteration at depth {}", depth + 1);
×
559
                schedule(s, LOAD_DECLARATIONS.with("depth", depth + 1));
×
560
            }
561

562
        }
×
563

564
    },
565

566
    CALCULATE_TRUST_SCORES {
33✔
567

568
        // DB read from: accounts, trustPaths
569
        // DB write to:  accounts
570

571
        public void run(ClientSession s, Document taskDoc) {
572

573
            while (true) {
574
                Document d = getOne(s, "accounts_loading", new Document("status", expanded.getValue()));
×
575
                if (d == null) {
×
576
                    break;
×
577
                }
578

579
                double ratio = 0.0;
×
580
                Map<String, Boolean> seenPathElements = new HashMap<>();
×
581
                int pathCount = 0;
×
582
                try (MongoCursor<Document> trustPaths = collection("trustPaths_loading").find(s,
×
583
                        new Document("agent", d.get("agent").toString()).append("pubkey", d.get("pubkey").toString())
×
584
                ).sort(orderBy(ascending("depth"), descending("ratio"), ascending("sorthash"))).cursor()) {
×
585
                    while (trustPaths.hasNext()) {
×
586
                        Document trustPath = trustPaths.next();
×
587
                        ratio += trustPath.getDouble("ratio");
×
588
                        boolean independentPath = true;
×
589
                        String[] pathElements = trustPath.getString("_id").split(" ");
×
590
                        // Iterate over path elements, ignoring first (root) and last (this agent/pubkey):
591
                        for (int i = 1; i < pathElements.length - 1; i++) {
×
592
                            String p = pathElements[i];
×
593
                            if (seenPathElements.containsKey(p)) {
×
594
                                independentPath = false;
×
595
                                break;
×
596
                            }
597
                            seenPathElements.put(p, true);
×
598
                        }
599
                        if (independentPath) {
×
600
                            pathCount += 1;
×
601
                        }
602
                    }
×
603
                }
604
                double rawQuota = GLOBAL_QUOTA * ratio;
×
605
                int quota = (int) rawQuota;
×
606
                if (rawQuota < MIN_USER_QUOTA) {
×
607
                    quota = MIN_USER_QUOTA;
×
608
                } else if (rawQuota > MAX_USER_QUOTA) {
×
609
                    quota = MAX_USER_QUOTA;
×
610
                }
611
                set(s, "accounts_loading",
×
612
                        d.append("status", processed.getValue())
×
613
                                .append("ratio", ratio)
×
614
                                .append("pathCount", pathCount)
×
615
                                .append("quota", quota)
×
616
                );
617
            }
×
618
            schedule(s, AGGREGATE_AGENTS);
×
619

620
        }
×
621

622
    },
623

624
    AGGREGATE_AGENTS {
33✔
625

626
        // DB read from: accounts, agents
627
        // DB write to:  accounts, agents
628

629
        public void run(ClientSession s, Document taskDoc) {
630

631
            while (true) {
632
                Document a = getOne(s, "accounts_loading", new Document("status", processed.getValue()));
×
633
                if (a == null) {
×
634
                    break;
×
635
                }
636

637
                Document agentId = new Document("agent", a.get("agent").toString()).append("status", processed.getValue());
×
638
                int count = 0;
×
639
                int pathCountSum = 0;
×
640
                double totalRatio = 0.0d;
×
641
                // Canonical-name resolution across the agent's approved keys: pick the
642
                // row with MAX(ratio); ties broken on lex-min name for determinism.
643
                // Per-(agent, pubkey) name was already chosen by LOAD_ENDORSEMENTS as
644
                // "latest declaring intro wins"; this layer just folds across keys.
645
                String chosenName = null;
×
646
                double chosenRatio = Double.NEGATIVE_INFINITY;
×
647
                try (MongoCursor<Document> agentAccounts = collection("accounts_loading").find(s, agentId).cursor()) {
×
648
                    while (agentAccounts.hasNext()) {
×
649
                        Document d = agentAccounts.next();
×
650
                        count++;
×
651
                        pathCountSum += d.getInteger("pathCount");
×
652
                        double r = d.getDouble("ratio");
×
653
                        totalRatio += r;
×
654
                        String n = d.getString("name");
×
655
                        if (n != null && (r > chosenRatio
×
656
                                          || (r == chosenRatio && (chosenName == null || n.compareTo(chosenName) < 0)))) {
×
657
                            chosenName = n;
×
658
                            chosenRatio = r;
×
659
                        }
660
                    }
×
661
                }
662
                collection("accounts_loading").updateMany(s, agentId, new Document("$set",
×
663
                        new DbEntryWrapper(aggregated).getDocument()));
×
664
                insert(s, "agents_loading",
×
665
                        agentId.append("accountCount", count)
×
666
                                .append("avgPathCount", (double) pathCountSum / count)
×
667
                                .append("totalRatio", totalRatio)
×
668
                                .append("name", chosenName)
×
669
                );
670
            }
×
671
            schedule(s, ASSIGN_PUBKEYS);
×
672

673
        }
×
674

675
    },
676

677
    ASSIGN_PUBKEYS {
33✔
678

679
        // DB read from: accounts
680
        // DB write to:  accounts
681

682
        public void run(ClientSession s, Document taskDoc) {
683

684
            while (true) {
685
                Document a = getOne(s, "accounts_loading", new DbEntryWrapper(aggregated).getDocument());
×
686
                if (a == null) {
×
687
                    break;
×
688
                }
689

690
                Document pubkeyId = new Document("pubkey", a.get("pubkey").toString());
×
691
                if (collection("accounts_loading").countDocuments(s, pubkeyId) == 1) {
×
692
                    collection("accounts_loading").updateMany(s, pubkeyId,
×
693
                            new Document("$set", new DbEntryWrapper(approved).getDocument()));
×
694
                } else {
695
                    // TODO At the moment all get marked as 'contested'; implement more nuanced algorithm
696
                    collection("accounts_loading").updateMany(s, pubkeyId, new Document("$set",
×
697
                            new DbEntryWrapper(contested).getDocument()));
×
698
                }
699
            }
×
700
            schedule(s, DETERMINE_UPDATES);
×
701

702
        }
×
703

704
    },
705

706
    DETERMINE_UPDATES {
33✔
707

708
        // DB read from: accounts
709
        // DB write to:  accounts
710

711
        public void run(ClientSession s, Document taskDoc) {
712

713
            // TODO Handle contested accounts properly:
714
            for (Document d : collection("accounts_loading").find(
×
715
                    new DbEntryWrapper(approved).getDocument())) {
×
716
                // TODO Consider quota too:
717
                Document accountId = new Document("agent", d.get("agent").toString()).append("pubkey", d.get("pubkey").toString());
×
718
                if (collection(Collection.ACCOUNTS.toString()) == null || !has(s, Collection.ACCOUNTS.toString(),
×
719
                        accountId.append("status", loaded.getValue()))) {
×
720
                    set(s, "accounts_loading", d.append("status", toLoad.getValue()));
×
721
                } else {
722
                    set(s, "accounts_loading", d.append("status", loaded.getValue()));
×
723
                }
724
            }
×
725
            schedule(s, FINALIZE_TRUST_STATE);
×
726

727
        }
×
728

729
    },
730

731
    FINALIZE_TRUST_STATE {
33✔
732
        // We do this is a separate task/transaction, because if we do it at the beginning of RELEASE_DATA, that task hangs and cannot
733
        // properly re-run (as some renaming outside of transactions will have taken place).
734
        public void run(ClientSession s, Document taskDoc) {
735
            String newTrustStateHash = RegistryDB.calculateTrustStateHash(s);
×
736
            String previousTrustStateHash = (String) getValue(s, Collection.SERVER_INFO.toString(), "trustStateHash");  // may be null
×
737
            setValue(s, Collection.SERVER_INFO.toString(), "lastTrustStateUpdate", ZonedDateTime.now().toString());
×
738

739
            schedule(s, RELEASE_DATA.with("newTrustStateHash", newTrustStateHash).append("previousTrustStateHash", previousTrustStateHash));
×
740
        }
×
741

742
    },
743

744
    RELEASE_DATA {
33✔
745

746
        private static final int TRUST_STATE_SNAPSHOT_RETENTION = 100;
747

748
        public void run(ClientSession s, Document taskDoc) {
749
            ServerStatus status = getServerStatus(s);
×
750

751
            String newTrustStateHash = taskDoc.get("newTrustStateHash").toString();
×
752
            String previousTrustStateHash = taskDoc.getString("previousTrustStateHash");  // may be null
×
753

754
            // Renaming collections is run outside of a transaction, but is idempotent operation, so can safely be retried if task fails:
755
            rename("accounts_loading", Collection.ACCOUNTS.toString());
×
756
            rename("trustPaths_loading", "trustPaths");
×
757
            rename("agents_loading", Collection.AGENTS.toString());
×
758
            rename("endorsements_loading", "endorsements");
×
759

760
            if (previousTrustStateHash == null || !previousTrustStateHash.equals(newTrustStateHash)) {
×
761
                increaseStateCounter(s);
×
762
                setValue(s, Collection.SERVER_INFO.toString(), "trustStateHash", newTrustStateHash);
×
763
                Object trustStateCounter = getValue(s, Collection.SERVER_INFO.toString(), "trustStateCounter");
×
764
                insert(s, "debug_trustPaths", new Document()
×
765
                        .append("trustStateTxt", DebugPage.getTrustPathsTxt(s))
×
766
                        .append("trustStateHash", newTrustStateHash)
×
767
                        .append("trustStateCounter", trustStateCounter)
×
768
                );
769

770
                // Structured hash-keyed snapshot for consumer mirroring (#107).
771
                // Reads the accounts collection just renamed from accounts_loading above (:697).
772
                List<Document> snapshotAccounts = new ArrayList<>();
×
773
                for (Document a : collection(Collection.ACCOUNTS.toString()).find(s)) {
×
774
                    String pubkey = a.getString("pubkey");
×
775
                    if ("$".equals(pubkey)) {
×
776
                        continue;
×
777
                    }
778
                    snapshotAccounts.add(new Document()
×
779
                            .append("pubkey", pubkey)
×
780
                            .append("agent", a.getString("agent"))
×
781
                            .append("name", a.getString("name"))
×
782
                            .append("nameCreatedAt", a.get("nameCreatedAt"))
×
783
                            .append("status", a.getString("status"))
×
784
                            .append("depth", a.get("depth"))
×
785
                            .append("pathCount", a.get("pathCount"))
×
786
                            .append("ratio", a.get("ratio"))
×
787
                            .append("quota", a.get("quota")));
×
788
                }
×
789
                Document snapshot = new Document()
×
790
                        .append("_id", newTrustStateHash)
×
791
                        .append("trustStateCounter", trustStateCounter)
×
792
                        .append("createdAt", ZonedDateTime.now().toString())
×
793
                        .append("accounts", snapshotAccounts);
×
794
                collection(Collection.TRUST_STATE_SNAPSHOTS.toString()).replaceOne(
×
795
                        s,
796
                        new Document("_id", newTrustStateHash),
797
                        snapshot,
798
                        new ReplaceOptions().upsert(true));
×
799

800
                // Prune beyond retention: collect _ids of snapshots past the Nth most recent, delete them.
801
                // trustStateCounter is monotonically increasing (see increaseStateCounter above), so ordering is well-defined.
802
                List<Object> toPrune = new ArrayList<>();
×
803
                try (MongoCursor<Document> stale = collection(Collection.TRUST_STATE_SNAPSHOTS.toString())
×
804
                        .find(s)
×
805
                        .sort(descending("trustStateCounter"))
×
806
                        .skip(TRUST_STATE_SNAPSHOT_RETENTION)
×
807
                        .projection(new Document("_id", 1))
×
808
                        .cursor()) {
×
809
                    while (stale.hasNext()) {
×
810
                        toPrune.add(stale.next().get("_id"));
×
811
                    }
812
                }
813
                if (!toPrune.isEmpty()) {
×
814
                    collection(Collection.TRUST_STATE_SNAPSHOTS.toString()).deleteMany(
×
815
                            s, new Document("_id", new Document("$in", toPrune)));
816
                }
817
            }
818

819
            if (status == coreLoading) {
×
820
                setServerStatus(s, coreReady);
×
821
            } else {
822
                setServerStatus(s, ready);
×
823
            }
824

825
            // Run update after 1h:
826
            schedule(s, UPDATE.withDelay(60 * 60 * 1000));
×
827
        }
×
828

829
    },
830

831
    UPDATE {
33✔
832
        public void run(ClientSession s, Document taskDoc) {
833
            ServerStatus status = getServerStatus(s);
×
834
            if (status == ready || status == coreReady) {
×
835
                setServerStatus(s, updating);
×
836
                schedule(s, INIT_COLLECTIONS);
×
837
            } else {
838
                logger.info("Postponing update; currently in status {}", status);
×
839
                schedule(s, UPDATE.withDelay(10 * 60 * 1000));
×
840
            }
841

842
        }
×
843

844
    },
845

846
    LOAD_FULL {
33✔
847
        public void run(ClientSession s, Document taskDoc) {
848
            logger.debug("LOAD_FULL invoked; taskDoc={}", taskDoc);
12✔
849

850
            if ("false".equals(System.getenv("REGISTRY_PERFORM_FULL_LOAD"))) {
15!
851
                logger.info("REGISTRY_PERFORM_FULL_LOAD=false; skipping full load");
×
852
                return;
×
853
            }
854

855
            ServerStatus status = getServerStatus(s);
9✔
856
            logger.debug("Server status check for LOAD_FULL: status={}", status);
12✔
857
            if (status != coreReady && status != ready && status != updating) {
27!
858
                logger.info("Server status={} is not eligible for full load (expected coreReady/ready/updating); deferring and retrying in 1000ms", status);
12✔
859
                schedule(s, LOAD_FULL.withDelay(1000));
15✔
860
                return;
3✔
861
            }
862

863
            Document a = getOne(s, Collection.ACCOUNTS.toString(), new DbEntryWrapper(toLoad).getDocument());
×
864
            if (a == null) {
×
865
                logger.info("No accounts left with status={}; full load pass complete", toLoad);
×
866
                if (status == coreReady) {
×
867
                    logger.info("Server status transitioning coreReady -> ready; full load finished");
×
868
                    setServerStatus(s, ready);
×
869
                }
870
                logger.info("Scheduling optional loading checks (RUN_OPTIONAL_LOAD) in 100ms");
×
871
                schedule(s, RUN_OPTIONAL_LOAD.withDelay(100));
×
872
            } else {
873
                final String ph = a.getString("pubkey");
×
874
                boolean quotaReached = false;
×
875
                if (!ph.equals("$")) {
×
876
                    if (!AgentFilter.isAllowed(s, ph)) {
×
877
                        logger.info("Pubkey {} is not covered by the agent filter; marking account as skipped", ph);
×
878
                        set(s, Collection.ACCOUNTS.toString(), a.append("status", skipped.getValue()));
×
879
                        schedule(s, LOAD_FULL.withDelay(100));
×
880
                        return;
×
881
                    }
882
                    if (AgentFilter.isOverQuota(s, ph)) {
×
883
                        logger.info("Pubkey {} is already over quota; skipping load, marking account as capped", ph);
×
884
                        quotaReached = true;
×
885
                    } else {
886
                        long startTime = System.nanoTime();
×
887
                        AtomicLong totalLoaded = new AtomicLong(0);
×
888

889
                        // Load per covered type (or "$" if no restriction) with checksum skip-ahead
890
                        for (String typeHash : getLoadTypeHashes(s, ph)) {
×
891
                            logger.debug("Pubkey {}: starting load for typeHash={}", ph, typeHash);
×
892
                            String checksums = buildChecksumFallbacks(s, ph, typeHash);
×
893
                            logger.debug("Pubkey {}, typeHash={}: checksum fallbacks={}", ph, typeHash, checksums);
×
894
                            try (var stream = NanopubLoader.retrieveNanopubsFromPeers(typeHash, ph, checksums)) {
×
895
                                NanopubLoader.loadStreamInParallel(stream, np -> {
×
896
                                    if (!CoverageFilter.isCovered(np)) {
×
897
                                        logger.debug("Pubkey {}: nanopub {} not covered; skipping", ph, np);
×
898
                                        return;
×
899
                                    }
900
                                    try (ClientSession ws = RegistryDB.getClient().startSession()) {
×
901
                                        if (!AgentFilter.isOverQuota(ws, ph)) {
×
902
                                            loadNanopub(ws, np, ph, "$");
×
903
                                            totalLoaded.incrementAndGet();
×
904
                                        } else {
905
                                            logger.debug("Pubkey {} hit quota mid-stream; skipping nanopub {}", ph, np);
×
906
                                        }
907
                                    }
908
                                });
×
909
                            }
910
                            logger.debug("Pubkey {}: finished load for typeHash={}", ph, typeHash);
×
911
                        }
×
912

913
                        double timeSeconds = (System.nanoTime() - startTime) * 1e-9;
×
914
                        logger.info("Pubkey {}: loaded {} nanopubs in {}s ({} np/s)",
×
915
                                ph, totalLoaded.get(), String.format("%.2f", timeSeconds),
×
916
                                String.format("%.2f", totalLoaded.get() / timeSeconds));
×
917

918
                        if (AgentFilter.isOverQuota(s, ph)) {
×
919
                            logger.info("Pubkey {} reached quota during this load; marking account as capped", ph);
×
920
                            quotaReached = true;
×
921
                        }
922
                    }
×
923
                } else {
924
                    logger.debug("Account pubkey is '$' (unrestricted); no per-pubkey quota/filter checks applied");
×
925
                }
926

927
                Document l = getOne(s, "lists", new Document().append("pubkey", ph).append("type", "$"));
×
928
                if (l != null) {
×
929
                    logger.debug("Pubkey {}: marking matching list entry as loaded", ph);
×
930
                    set(s, "lists", l.append("status", loaded.getValue()));
×
931
                }
932
                EntryStatus accountStatus = quotaReached ? capped : loaded;
×
933
                int effectiveQuota = AgentFilter.getQuota(s, ph);
×
934
                if (effectiveQuota >= 0) {
×
935
                    logger.debug("Pubkey {}: recording effective quota={}", ph, effectiveQuota);
×
936
                    a.append("quota", effectiveQuota);
×
937
                }
938
                logger.info("Pubkey {}: account load complete, status={}", ph, accountStatus);
×
939
                set(s, Collection.ACCOUNTS.toString(), a.append("status", accountStatus.getValue()));
×
940

941
                schedule(s, LOAD_FULL.withDelay(100));
×
942
            }
943
        }
×
944

945
        @Override
946
        public boolean runAsTransaction() {
947
            // TODO Make this a transaction once we connect to other Nanopub Registry instances:
948
            return false;
×
949
        }
950

951
    },
952

953
    RUN_OPTIONAL_LOAD {
33✔
954

955
        private static final int BATCH_SIZE = Integer.parseInt(
15✔
956
                Utils.getEnv("REGISTRY_OPTIONAL_LOAD_BATCH_SIZE", "100"));
3✔
957

958
        public void run(ClientSession s, Document taskDoc) {
959
            if ("false".equals(System.getenv("REGISTRY_ENABLE_OPTIONAL_LOAD"))) {
×
960
                schedule(s, CHECK_NEW.withDelay(500));
×
961
                return;
×
962
            }
963

964
            AtomicLong totalLoaded = new AtomicLong(0);
×
965

966
            // Phase 1: Process encountered intro lists (core loading)
967
            while (totalLoaded.get() < BATCH_SIZE) {
×
968
                Document di = getOne(s, "lists", new Document("type", INTRO_TYPE_HASH).append("status", encountered.getValue()));
×
969
                if (di == null) {
×
970
                    break;
×
971
                }
972

973
                final String pubkeyHash = di.getString("pubkey");
×
974
                Validate.notNull(pubkeyHash);
×
975
                logger.info("Optional core loading: {}", pubkeyHash);
×
976

977
                String introChecksums = buildChecksumFallbacks(s, pubkeyHash, INTRO_TYPE_HASH);
×
978
                try (var stream = NanopubLoader.retrieveNanopubsFromPeers(INTRO_TYPE_HASH, pubkeyHash, introChecksums)) {
×
979
                    NanopubLoader.loadStreamInParallel(stream, np -> {
×
980
                        try (ClientSession ws = RegistryDB.getClient().startSession()) {
×
981
                            loadNanopub(ws, np, pubkeyHash, INTRO_TYPE);
×
982
                            totalLoaded.incrementAndGet();
×
983
                        }
984
                    });
×
985
                }
986
                set(s, "lists", di.append("status", loaded.getValue()));
×
987

988
                String endorseChecksums = buildChecksumFallbacks(s, pubkeyHash, ENDORSE_TYPE_HASH);
×
989
                try (var stream = NanopubLoader.retrieveNanopubsFromPeers(ENDORSE_TYPE_HASH, pubkeyHash, endorseChecksums)) {
×
990
                    NanopubLoader.loadStreamInParallel(stream, np -> {
×
991
                        try (ClientSession ws = RegistryDB.getClient().startSession()) {
×
992
                            loadNanopub(ws, np, pubkeyHash, ENDORSE_TYPE);
×
993
                            totalLoaded.incrementAndGet();
×
994
                        }
995
                    });
×
996
                }
997

998
                Document de = new Document("pubkey", pubkeyHash).append("type", ENDORSE_TYPE_HASH);
×
999
                if (has(s, "lists", de)) {
×
1000
                    set(s, "lists", de.append("status", loaded.getValue()));
×
1001
                } else {
1002
                    insert(s, "lists", de.append("status", loaded.getValue()));
×
1003
                }
1004

1005
                Document df = new Document("pubkey", pubkeyHash).append("type", "$");
×
1006
                if (!has(s, "lists", df)) {
×
1007
                    insert(s, "lists", df.append("status", encountered.getValue()));
×
1008
                }
1009
            }
×
1010

1011
            // Phase 2: Process encountered full lists (if budget remains)
1012
            while (totalLoaded.get() < BATCH_SIZE) {
×
1013
                Document df = getOne(s, "lists", new Document("type", "$").append("status", encountered.getValue()));
×
1014
                if (df == null) {
×
1015
                    break;
×
1016
                }
1017

1018
                final String pubkeyHash = df.getString("pubkey");
×
1019
                logger.info("Optional full loading: {}", pubkeyHash);
×
1020

1021
                // Load per covered type (or "$" if no restriction) with checksum skip-ahead
1022
                for (String typeHash : getLoadTypeHashes(s, pubkeyHash)) {
×
1023
                    String checksums = buildChecksumFallbacks(s, pubkeyHash, typeHash);
×
1024
                    try (var stream = NanopubLoader.retrieveNanopubsFromPeers(typeHash, pubkeyHash, checksums)) {
×
1025
                        NanopubLoader.loadStreamInParallel(stream, np -> {
×
1026
                            if (!CoverageFilter.isCovered(np)) {
×
1027
                                return;
×
1028
                            }
1029
                            try (ClientSession ws = RegistryDB.getClient().startSession()) {
×
1030
                                loadNanopub(ws, np, pubkeyHash, "$");
×
1031
                                totalLoaded.incrementAndGet();
×
1032
                            }
1033
                        });
×
1034
                    }
1035
                }
×
1036

1037
                set(s, "lists", df.append("status", loaded.getValue()));
×
1038

1039
                // Backfill nanopubs stored locally during the transitional period (i.e. before
1040
                // the $ list was loaded). Such nanopubs were stored in the nanopubs collection by
1041
                // simpleLoad() but never added to listEntries; add them to the $ list now.
1042
                logger.info("Backfilling locally stored nanopubs for pubkey: {}", pubkeyHash);
×
1043
                try (MongoCursor<Document> npCursor = collection(Collection.NANOPUBS.toString())
×
1044
                        .find(s, new Document("pubkey", pubkeyHash)).cursor()) {
×
1045
                    while (npCursor.hasNext()) {
×
1046
                        String fullId = npCursor.next().getString("fullId");
×
1047
                        if (fullId == null) {
×
1048
                            continue;
×
1049
                        }
1050
                        try (ClientSession ws = RegistryDB.getClient().startSession()) {
×
1051
                            Nanopub np = NanopubLoader.retrieveLocalNanopub(ws, fullId);
×
1052
                            if (np != null && CoverageFilter.isCovered(np)) {
×
1053
                                loadNanopub(ws, np, pubkeyHash, "$");
×
1054
                                totalLoaded.incrementAndGet();
×
1055
                            }
1056
                        } catch (Exception ex) {
×
1057
                            logger.info("Error backfilling nanopub {}: {}", fullId, ex.getMessage());
×
1058
                        }
×
1059
                    }
×
1060
                }
1061
            }
×
1062

1063
            if (totalLoaded.get() > 0) {
×
1064
                logger.info("Optional load batch completed: {} nanopubs across multiple pubkeys", totalLoaded.get());
×
1065
            }
1066

1067
            if (prioritizeAllPubkeys()) {
×
1068
                // Check if there are more pubkeys waiting to be processed
1069
                boolean moreWork = has(s, "lists", new Document("type", INTRO_TYPE_HASH).append("status", encountered.getValue()))
×
1070
                                   || has(s, "lists", new Document("type", "$").append("status", encountered.getValue()));
×
1071
                if (moreWork) {
×
1072
                    // Continue processing without a full CHECK_NEW cycle in between.
1073
                    // CHECK_NEW will run naturally once all encountered lists are processed.
1074
                    schedule(s, RUN_OPTIONAL_LOAD.withDelay(10));
×
1075
                } else {
1076
                    schedule(s, CHECK_NEW.withDelay(500));
×
1077
                }
1078
            } else {
×
1079
                // Throttled: yield to CHECK_NEW after each batch to prioritize approved pubkeys
1080
                schedule(s, CHECK_NEW.withDelay(500));
×
1081
            }
1082
        }
×
1083

1084
    },
1085

1086
    CHECK_NEW {
33✔
1087
        public void run(ClientSession s, Document taskDoc) {
1088
            RegistryPeerConnector.checkPeers(s);
×
1089
            // Keep legacy connection during transition period:
1090
            LegacyConnector.checkForNewNanopubs(s);
×
1091
            // TODO Somehow throttle the loading of such potentially non-approved nanopubs
1092

1093
            schedule(s, LOAD_FULL.withDelay(100));
×
1094
        }
×
1095

1096
        @Override
1097
        public boolean runAsTransaction() {
1098
            // Peer sync includes long-running streaming fetches that would exceed
1099
            // MongoDB's transaction timeout; each operation is individually safe.
1100
            return false;
×
1101
        }
1102

1103
    };
1104

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

1107
    public abstract void run(ClientSession s, Document taskDoc) throws Exception;
1108

1109
    public boolean runAsTransaction() {
1110
        return true;
×
1111
    }
1112

1113
    Document asDocument() {
1114
        return withDelay(0L);
12✔
1115
    }
1116

1117
    private Document withDelay(long delay) {
1118
        // TODO Rename "not-before" to "notBefore" for consistency with other field names
1119
        return new Document()
15✔
1120
                .append("not-before", System.currentTimeMillis() + delay)
21✔
1121
                .append("action", name());
6✔
1122
    }
1123

1124
    private Document with(String key, Object value) {
1125
        return asDocument().append(key, value);
×
1126
    }
1127

1128
    private static boolean prioritizeAllPubkeys() {
1129
        return "true".equals(System.getenv("REGISTRY_PRIORITIZE_ALL_PUBKEYS"));
×
1130
    }
1131

1132
    /**
1133
     * Returns the type hashes to load for a given pubkey. When coverage is unrestricted,
1134
     * returns just "$" (all types in one request). When restricted, returns each covered
1135
     * type hash for per-type fetching with checksum skip-ahead.
1136
     * <p>
1137
     * TODO: Fetching "$" from peers with type restrictions will only return their covered
1138
     * types, not all types. To get full coverage, we'd need to fetch per-type from such peers.
1139
     * Additionally, checksum-based skip-ahead won't work correctly against such peers, because
1140
     * their "$" list has different checksums due to the differing type subset. This means full
1141
     * re-downloads on every cycle. Per-type fetching would solve both issues.
1142
     */
1143
    private static java.util.List<String> getLoadTypeHashes(ClientSession s, String pubkeyHash) {
1144
        if (CoverageFilter.coversAllTypes()) {
×
1145
            return java.util.List.of("$");
×
1146
        }
1147
        return java.util.List.copyOf(CoverageFilter.getCoveredTypeHashes());
×
1148
    }
1149

1150
    // TODO Move these to setting:
1151
    private static final int MAX_TRUST_PATH_DEPTH = 10;
1152
    private static final double MIN_TRUST_PATH_RATIO = 0.0000000001;
1153
    //private static final double MIN_TRUST_PATH_RATIO = 0.01; // For testing
1154
    private static final int GLOBAL_QUOTA = Integer.parseInt(
12✔
1155
            Utils.getEnv("REGISTRY_GLOBAL_QUOTA", "1000000000"));
3✔
1156
    private static final int MIN_USER_QUOTA = Integer.parseInt(
12✔
1157
            Utils.getEnv("REGISTRY_MIN_USER_QUOTA", "1000"));
3✔
1158
    private static final int MAX_USER_QUOTA = Integer.parseInt(
12✔
1159
            Utils.getEnv("REGISTRY_MAX_USER_QUOTA", "100000"));
3✔
1160

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

1163
    private static volatile String currentTaskName;
1164
    private static volatile long currentTaskStartTime;
1165

1166
    public static String getCurrentTaskName() {
1167
        return currentTaskName;
×
1168
    }
1169

1170
    public static long getCurrentTaskStartTime() {
1171
        return currentTaskStartTime;
×
1172
    }
1173

1174
    /**
1175
     * The super important base entry point!
1176
     */
1177
    static void runTasks() {
1178
        try (ClientSession s = RegistryDB.getClient().startSession()) {
×
1179
            if (!RegistryDB.isInitialized(s)) {
×
1180
                schedule(s, INIT_DB); // does not yet execute, only schedules
×
1181
            }
1182

1183
            logger.info("Task runner started");
×
1184
            while (true) {
1185
                FindIterable<Document> taskResult = tasksCollection.find(s).sort(ascending("not-before"));
×
1186
                Document taskDoc = taskResult.first();
×
1187
                long sleepTime = 10;
×
1188
                if (taskDoc != null && taskDoc.getLong("not-before") < System.currentTimeMillis()) {
×
1189
                    Task task = valueOf(taskDoc.getString("action"));
×
1190
                    Object taskId = taskDoc.getOrDefault("_id", null);
×
1191
                    logger.info("Picked task to run: {} (docId={})", task.name(), taskId);
×
1192

1193
                    if (task.runAsTransaction()) {
×
1194
                        try {
1195
                            s.startTransaction();
×
1196
                            logger.debug("Transaction started for task {}", task.name());
×
1197
                            runTask(task, taskDoc);
×
1198
                            s.commitTransaction();
×
1199
                            logger.info("Transaction committed for task {}", task.name());
×
1200
                        } catch (Exception ex) {
×
1201
                            logger.warn("Transactional task {} failed, aborting: {}", task.name(), ex.getMessage(), ex);
×
1202
                            abortTransaction(s, ex.getMessage());
×
1203
                            logger.info("Transaction aborted for task {}", task.name());
×
1204
                            sleepTime = 1000;
×
1205
                        } finally {
1206
                            cleanTransactionWithRetry(s);
×
1207
                        }
×
1208
                    } else {
1209
                        try {
1210
                            runTask(task, taskDoc);
×
1211
                        } catch (Exception ex) {
×
1212
                            logger.warn("Non-transactional task {} failed: {}", task.name(), ex.getMessage(), ex);
×
1213
                        }
×
1214
                    }
1215
                }
1216
                try {
1217
                    Thread.sleep(sleepTime);
×
1218
                } catch (InterruptedException ex) {
×
1219
                    // ignore
1220
                    logger.debug("Task runner sleep interrupted");
×
1221
                }
×
1222
            }
×
1223
        }
1224
    }
1225

1226
    static void runTask(Task task, Document taskDoc) throws Exception {
1227
        currentTaskName = task.name();
9✔
1228
        currentTaskStartTime = System.currentTimeMillis();
6✔
1229
        logger.info("Starting task {} with request: {}", currentTaskName, taskDoc);
15✔
1230

1231
        try (ClientSession s = RegistryDB.getClient().startSession()) {
9✔
1232
            task.run(s, taskDoc);
12✔
1233
            long duration = System.currentTimeMillis() - currentTaskStartTime;
12✔
1234
            tasksCollection.deleteOne(s, eq("_id", taskDoc.get("_id")));
27✔
1235
            logger.info("Completed and removed from queue task {} in {} ms", currentTaskName, duration);
18✔
1236
        } catch (Exception ex) {
×
1237
            long duration = System.currentTimeMillis() - currentTaskStartTime;
×
1238
            logger.error("Task {} failed after {} ms: {}", currentTaskName, duration, ex.getMessage(), ex);
×
1239
            throw ex;
×
1240
        } finally {
1241
            currentTaskName = null;
6✔
1242
        }
1243
    }
3✔
1244

1245
    public static void abortTransaction(ClientSession mongoSession, String message) {
1246
        boolean successful = false;
×
1247
        while (!successful) {
×
1248
            try {
1249
                if (mongoSession.hasActiveTransaction()) {
×
1250
                    logger.info("Attempting to abort transaction: {}", message);
×
1251
                    mongoSession.abortTransaction();
×
1252
                }
1253
                successful = true;
×
1254
                logger.debug("abortTransaction succeeded");
×
1255
            } catch (Exception ex) {
×
1256
                logger.warn("abortTransaction attempt failed: {}", ex.getMessage(), ex);
×
1257
                try {
1258
                    Thread.sleep(100);
×
1259
                } catch (InterruptedException ie) {
×
1260
                    logger.debug("abortTransaction sleep interrupted");
×
1261
                }
×
1262
            }
×
1263
        }
1264
    }
×
1265

1266
    public synchronized static void cleanTransactionWithRetry(ClientSession mongoSession) {
1267
        boolean successful = false;
×
1268
        while (!successful) {
×
1269
            try {
1270
                if (mongoSession.hasActiveTransaction()) {
×
1271
                    mongoSession.abortTransaction();
×
1272
                    logger.debug("abortTransaction during cleanup succeeded");
×
1273
                }
1274
                successful = true;
×
1275
                logger.debug("Transaction cleanup completed");
×
1276
            } catch (Exception ex) {
×
1277
                logger.warn("Transaction cleanup failed: {}", ex.getMessage(), ex);
×
1278
                try {
1279
                    Thread.sleep(100);
×
1280
                } catch (InterruptedException ie) {
×
1281
                    logger.debug("cleanTransactionWithRetry sleep interrupted");
×
1282
                }
×
1283
            }
×
1284
        }
1285
    }
×
1286

1287
    private static IntroNanopub getAgentIntro(ClientSession mongoSession, String nanopubId) {
1288
        IntroNanopub agentIntro = new IntroNanopub(NanopubLoader.retrieveNanopub(mongoSession, nanopubId));
×
1289
        if (agentIntro.getUser() == null) {
×
1290
            return null;
×
1291
        }
1292
        loadNanopub(mongoSession, agentIntro.getNanopub());
×
1293
        return agentIntro;
×
1294
    }
1295

1296

1297
    private static void setServerStatus(ClientSession mongoSession, ServerStatus status) {
1298
        setValue(mongoSession, Collection.SERVER_INFO.toString(), "status", status.toString());
21✔
1299
    }
3✔
1300

1301
    private static ServerStatus getServerStatus(ClientSession mongoSession) {
1302
        Object status = getValue(mongoSession, Collection.SERVER_INFO.toString(), "status");
18✔
1303
        if (status == null) {
6!
1304
            throw new RuntimeException("Illegal DB state: serverInfo status unavailable");
×
1305
        }
1306
        return ServerStatus.valueOf(status.toString());
12✔
1307
    }
1308

1309
    private static void schedule(ClientSession mongoSession, Task task) {
1310
        schedule(mongoSession, task.asDocument());
12✔
1311
    }
3✔
1312

1313
    private static void schedule(ClientSession mongoSession, Document taskDoc) {
1314
        logger.info("Scheduling task: {}", taskDoc.getString("action"));
18✔
1315
        tasksCollection.insertOne(mongoSession, taskDoc);
12✔
1316
    }
3✔
1317

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

© 2026 Coveralls, Inc