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

knowledgepixels / nanopub-query / 30458093186

29 Jul 2026 01:52PM UTC coverage: 58.203% (-1.4%) from 59.566%
30458093186

push

github

web-flow
Merge pull request #144 from knowledgepixels/feat/retro-sweep

feat(reconciler): retro pass for verified-then-lost shards + healthcheck restart softening

620 of 1202 branches covered (51.58%)

Branch coverage included in aggregate %.

1796 of 2949 relevant lines covered (60.9%)

9.43 hits per line

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

80.2
src/main/java/com/knowledgepixels/query/MetricsCollector.java
1
package com.knowledgepixels.query;
2

3
import io.micrometer.core.instrument.Gauge;
4
import io.micrometer.core.instrument.MeterRegistry;
5

6
import java.util.Map;
7
import java.util.Set;
8
import java.util.concurrent.ConcurrentHashMap;
9
import java.util.concurrent.atomic.AtomicInteger;
10

11
/**
12
 * Class to collect metrics for performance analysis.
13
 */
14
public final class MetricsCollector {
15

16
    private final AtomicInteger loadCounter = new AtomicInteger(0);
18✔
17
    private final AtomicInteger typeRepositoriesCounter = new AtomicInteger(0);
18✔
18
    private final AtomicInteger pubkeyRepositoriesCounter = new AtomicInteger(0);
18✔
19
    private final AtomicInteger fullRepositoriesCounter = new AtomicInteger(0);
18✔
20

21
    private final Map<StatusController.State, AtomicInteger> statusStates = new ConcurrentHashMap<>();
15✔
22

23
    /**
24
     * Creates new metrics collector object.
25
     *
26
     * @param meterRegistry The registry instance
27
     */
28
    public MetricsCollector(MeterRegistry meterRegistry) {
6✔
29
        // Numeric metrics
30
        Gauge.builder("registry.load.counter", loadCounter, AtomicInteger::get).register(meterRegistry);
24✔
31
        Gauge.builder("registry.type.repositories.counter", typeRepositoriesCounter, AtomicInteger::get).register(meterRegistry);
24✔
32
        Gauge.builder("registry.pubkey.repositories.counter", pubkeyRepositoriesCounter, AtomicInteger::get).register(meterRegistry);
24✔
33
        Gauge.builder("registry.full.repositories.counter", fullRepositoriesCounter, AtomicInteger::get).register(meterRegistry);
24✔
34

35
        // Circuit-breaker observability: expose both the raw counter and a boolean
36
        // "breaker active" flag. The boolean is redundant with counter >= threshold
37
        // but much cleaner to visualise in Grafana (the counter can saturate well
38
        // above the threshold during a sustained outage, which makes a single
39
        // "is the breaker tripped?" alert awkward to express over the raw value).
40
        Gauge.builder("registry.loader.consecutive_batch_failures",
12✔
41
                        () -> (double) JellyNanopubLoader.consecutiveBatchFailures)
×
42
                .description("Consecutive loadUpdates batches that threw an exception before succeeding")
6✔
43
                .register(meterRegistry);
6✔
44
        Gauge.builder("registry.loader.breaker_active",
12✔
45
                        () -> JellyNanopubLoader.consecutiveBatchFailures >= JellyNanopubLoader.BREAKER_THRESHOLD ? 1.0 : 0.0)
×
46
                .description("1 if the loader circuit breaker is tripped (consecutive failures >= threshold), 0 otherwise")
6✔
47
                .register(meterRegistry);
6✔
48
        // Liveness signal that works without log access: seconds since the last
49
        // non-exceptional loadUpdates return. Counts both "loaded a batch" and
50
        // "caught up, nothing to do" as progress. An instance whose value climbs
51
        // unbounded while peers stay low is stuck on something the other
52
        // gauges don't capture.
53
        Gauge.builder("registry.loader.last_successful_batch_age_seconds",
12✔
54
                        () -> {
55
                            long t = JellyNanopubLoader.lastSuccessfulBatchAtMs;
×
56
                            if (t == 0L) return 0.0;    // not started yet
×
57
                            return (System.currentTimeMillis() - t) / 1000.0;
×
58
                        })
59
                .description("Seconds since the last non-exceptional loadUpdates return (idle or loading)")
6✔
60
                .register(meterRegistry);
6✔
61

62
        // Shard-reconciliation observability (issue #139). Both read volatile
63
        // counters kept by ShardReconciler — no SPARQL on the scrape path. Any
64
        // non-zero repaired value means the backend acknowledged a shard write
65
        // that was not durable, and deserves an alert.
66
        Gauge.builder("registry.reconciler.nanopubs_checked_total",
12✔
67
                        () -> (double) ShardReconciler.checkedNanopubCount)
×
68
                .description("Nanopubs whose shard fan-out was verified by the reconciliation sweep since process start")
6✔
69
                .register(meterRegistry);
6✔
70
        Gauge.builder("registry.reconciler.shards_repaired_total",
12✔
71
                        () -> (double) ShardReconciler.repairedShardCount)
×
72
                .description("Missing shard repos detected and re-loaded by the reconciliation sweep since process start")
6✔
73
                .register(meterRegistry);
6✔
74
        Gauge.builder("registry.reconciler.shards_relost_total",
12✔
75
                        () -> (double) ShardReconciler.relostShardCount)
×
76
                .description("Shards that a previous sweep verified present and that later vanished (backend revoked readable state, issue #142)")
6✔
77
                .register(meterRegistry);
6✔
78

79
        // Status label metrics
80
        for (final var status : StatusController.State.values()) {
48✔
81
            AtomicInteger stateGauge = new AtomicInteger(0);
15✔
82
            statusStates.put(status, stateGauge);
18✔
83
            Gauge.builder("registry.server.status", stateGauge, AtomicInteger::get)
15✔
84
                    .description("Server status (1 if current)")
9✔
85
                    .tag("status", status.name())
9✔
86
                    .register(meterRegistry);
6✔
87
        }
88

89
        // Spaces / AuthorityResolver gauges. These read volatile fields kept
90
        // by AuthorityResolver — no SPARQL on the scrape path. Each lambda
91
        // re-fetches the singleton to match the lazy-init pattern used by
92
        // the rest of the codebase.
93
        Gauge.builder("registry.spaces.subjects.admin_ris",
12✔
94
                        () -> (double) AuthorityResolver.get().getLastSubjectTotals().adminRIs())
18✔
95
                .description("Distinct admin gen:RoleInstantiation subjects in the current space-state graph (last build/cycle observation)")
6✔
96
                .register(meterRegistry);
6✔
97
        Gauge.builder("registry.spaces.subjects.attachment_ras",
12✔
98
                        () -> (double) AuthorityResolver.get().getLastSubjectTotals().attachmentRAs())
×
99
                .description("Distinct gen:RoleAssignment subjects in the current space-state graph (last build/cycle observation)")
6✔
100
                .register(meterRegistry);
6✔
101
        Gauge.builder("registry.spaces.subjects.non_admin_ris",
12✔
102
                        () -> (double) AuthorityResolver.get().getLastSubjectTotals().nonAdminRIs())
×
103
                .description("Distinct non-admin gen:RoleInstantiation subjects in the current space-state graph (last build/cycle observation)")
6✔
104
                .register(meterRegistry);
6✔
105
        Gauge.builder("registry.spaces.delta.last_inserted_triples",
12✔
106
                        () -> (double) AuthorityResolver.get().getLastInsertedTriplesTotal())
×
107
                .description("Total inserted triples across all five tiers in the most recent full build or incremental cycle")
6✔
108
                .register(meterRegistry);
6✔
109
        Gauge.builder("registry.spaces.rebuild.last_duration_seconds",
12✔
110
                        () -> AuthorityResolver.get().getLastFullBuildDurationMs() / 1000.0)
×
111
                .description("Wall-clock duration of the most recent full space-state build")
6✔
112
                .register(meterRegistry);
6✔
113
        Gauge.builder("registry.spaces.cycle.last_duration_seconds",
12✔
114
                        () -> AuthorityResolver.get().getLastIncrementalCycleDurationMs() / 1000.0)
×
115
                .description("Wall-clock duration of the most recent incremental space-state cycle that did work")
6✔
116
                .register(meterRegistry);
6✔
117
        Gauge.builder("registry.spaces.processed_up_to_lag",
12✔
118
                        () -> (double) AuthorityResolver.get().getLastProcessedUpToLag())
15✔
119
                .description("currentLoadCounter - processedUpTo observed at the start of the most recent incremental cycle (0 after a full build)")
6✔
120
                .register(meterRegistry);
6✔
121
    }
3✔
122

123
    /**
124
     * Updates the metrics based on the current state of the system.
125
     */
126
    public void updateMetrics() {
127
        // Update numeric metrics
128
        loadCounter.set((int) StatusController.get().getState().loadCounter);
21✔
129
        // Request repository names once, to avoid multiple calls
130
        var repoNames = TripleStore.get().getRepositoryNames();
9✔
131
        if (repoNames == null) {
6!
132
            repoNames = Set.of();
×
133
        }
134
        typeRepositoriesCounter.set(
12✔
135
                (int) repoNames
136
                        .stream()
6✔
137
                        .filter(repo -> repo.startsWith("type_"))
15✔
138
                        .count()
6✔
139
        );
140
        pubkeyRepositoriesCounter.set(
12✔
141
                (int) repoNames
142
                        .stream()
6✔
143
                        .filter(repo -> repo.startsWith("pubkey_"))
15✔
144
                        .count()
6✔
145
        );
146
        fullRepositoriesCounter.set(repoNames.size());
15✔
147

148
        // Update status gauge
149
        final var currentStatus = StatusController.get().getState().state;
12✔
150
        for (final var status : StatusController.State.values()) {
48✔
151
            statusStates.get(status).set(status.equals(currentStatus) ? 1 : 0);
33!
152
        }
153
    }
3✔
154
}
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