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

knowledgepixels / nanodash / 28022637110

23 Jun 2026 11:23AM UTC coverage: 26.541% (-0.008%) from 26.549%
28022637110

push

github

web-flow
Merge pull request #495 from knowledgepixels/fix/home-page-cold-cache-race

fix: prevent cold-cache race that breaks the home page

1555 of 6905 branches covered (22.52%)

Branch coverage included in aggregate %.

3423 of 11851 relevant lines covered (28.88%)

4.25 hits per line

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

72.0
src/main/java/com/knowledgepixels/nanodash/repository/MaintainedResourceRepository.java
1
package com.knowledgepixels.nanodash.repository;
2

3
import com.knowledgepixels.nanodash.ApiCache;
4
import com.knowledgepixels.nanodash.QueryApiAccess;
5
import com.knowledgepixels.nanodash.domain.MaintainedResource;
6
import com.knowledgepixels.nanodash.domain.MaintainedResourceFactory;
7
import com.knowledgepixels.nanodash.domain.Space;
8
import org.nanopub.extra.services.ApiResponse;
9
import org.nanopub.extra.services.ApiResponseEntry;
10
import org.nanopub.extra.services.QueryRef;
11

12
import java.util.ArrayList;
13
import java.util.Collections;
14
import java.util.HashMap;
15
import java.util.HashSet;
16
import java.util.List;
17
import java.util.Map;
18
import java.util.Set;
19

20
public class MaintainedResourceRepository {
21

22
    private static final MaintainedResourceRepository INSTANCE = new MaintainedResourceRepository();
15✔
23

24
    /**
25
     * Get the singleton instance of MaintainedResourceRepository.
26
     *
27
     * @return The singleton instance of MaintainedResourceRepository.
28
     */
29
    public static MaintainedResourceRepository get() {
30
        return INSTANCE;
6✔
31
    }
32

33
    private MaintainedResourceRepository() {
6✔
34
    }
3✔
35

36
    private static final class Snapshot {
37
        final Map<String, MaintainedResource> resourcesById;
38
        final Map<String, MaintainedResource> resourcesByNamespace;
39
        final Map<Space, List<MaintainedResource>> resourcesBySpace;
40

41
        Snapshot(Map<String, MaintainedResource> byId,
42
                 Map<String, MaintainedResource> byNs,
43
                 Map<Space, List<MaintainedResource>> bySpace) {
6✔
44
            this.resourcesById = byId;
9✔
45
            this.resourcesByNamespace = byNs;
9✔
46
            this.resourcesBySpace = bySpace;
9✔
47
        }
3✔
48

49
        static final Snapshot EMPTY = new Snapshot(
9✔
50
                Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap());
15✔
51
    }
52

53
    // Source of truth is ApiCache (60s TTL on its own); we memoise the derived
54
    // maps keyed by the ApiResponse instance identity so they get rebuilt
55
    // exactly when ApiCache returns a fresh response and not on every call.
56
    private volatile ApiResponse cachedFor;
57
    private volatile Snapshot snapshot = Snapshot.EMPTY;
9✔
58

59
    private Snapshot current() {
60
        ApiResponse resp = ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_MAINTAINED_RESOURCES), false);
21✔
61
        if (resp == null) {
6!
62
            return snapshot;
×
63
        }
64
        if (resp == cachedFor) {
12✔
65
            return snapshot;
9✔
66
        }
67
        synchronized (this) {
12✔
68
            if (resp == cachedFor) {
12!
69
                return snapshot;
×
70
            }
71
            Snapshot built = build(resp);
12✔
72
            if (built == null) {
6!
73
                // Spaces weren't loaded yet, so every resource would have been
74
                // dropped (each row's space resolved to null). Don't latch this
75
                // empty result against the current response identity — return what
76
                // we have and rebuild on the next access, once spaces are warm.
77
                return snapshot;
×
78
            }
79
            snapshot = built;
9✔
80
            cachedFor = resp;
9✔
81
            return built;
12✔
82
        }
83
    }
84

85
    /**
86
     * Build the resource lookup maps from the spaces-repo response. Pulls
87
     * server-validated {@code npa:isMaintainedBy} links from the current
88
     * space-state graph; only the most recent declaration per resource is kept
89
     * (the {@code get-maintained-resources} query orders by {@code DESC(?date)},
90
     * so the first row per resource wins).
91
     */
92
    private Snapshot build(ApiResponse resp) {
93
        Map<String, MaintainedResource> byId = new HashMap<>();
12✔
94
        Map<String, MaintainedResource> byNamespace = new HashMap<>();
12✔
95
        Map<Space, List<MaintainedResource>> bySpace = new HashMap<>();
12✔
96
        Set<String> seenResources = new HashSet<>();
12✔
97
        boolean droppedForMissingSpace = false;
6✔
98
        for (ApiResponseEntry r : resp.getData()) {
33✔
99
            String resourceId = r.get("resource");
12✔
100
            if (resourceId == null || resourceId.isEmpty()) continue;
15!
101
            if (!seenResources.add(resourceId)) continue; // first row (newest date) wins
15✔
102
            String spaceId = r.get("space");
12✔
103
            Space space = SpaceRepository.get().findById(spaceId);
12✔
104
            if (space == null) {
6!
105
                // Space not (yet) loaded — likely a cold/racing SpaceRepository
106
                // rather than a genuinely unknown space. Track it so current()
107
                // can avoid latching an under-built snapshot.
108
                if (spaceId != null && !spaceId.isEmpty()) droppedForMissingSpace = true;
×
109
                continue;
110
            }
111
            ApiResponseEntry entry = new ApiResponseEntry();
12✔
112
            entry.add("resource", resourceId);
12✔
113
            entry.add("np", r.get("np"));
18✔
114
            String label = r.get("label");
12✔
115
            if (label != null && !label.isEmpty()) entry.add("label", label);
27!
116
            String namespace = r.get("namespace");
12✔
117
            if (namespace != null && !namespace.isEmpty()) entry.add("namespace", namespace);
27!
118
            MaintainedResource resource = MaintainedResourceFactory.getOrCreate(entry, space);
12✔
119
            byId.put(resourceId, resource);
15✔
120
            bySpace.computeIfAbsent(space, k -> new ArrayList<>()).add(resource);
36✔
121
            if (resource.getNamespace() != null) {
9✔
122
                // TODO Handle conflicts when two resources claim the same namespace:
123
                byNamespace.put(resource.getNamespace(), resource);
18✔
124
            }
125
        }
3✔
126
        if (byId.isEmpty() && droppedForMissingSpace) {
9!
127
            // Every resource was dropped because its space wasn't resolvable, yet
128
            // the response did contain rows — SpaceRepository is cold. Signal "not
129
            // ready" so current() doesn't memoise this empty result.
130
            return null;
×
131
        }
132
        MaintainedResourceFactory.removeStale(byId.keySet());
9✔
133
        return new Snapshot(byId, byNamespace, bySpace);
21✔
134
    }
135

136
    /**
137
     * Find a maintained resource by its namespace.
138
     *
139
     * @param namespace The namespace to search for.
140
     * @return The MaintainedResource with the given namespace, or null if not found.
141
     */
142
    public MaintainedResource findByNamespace(String namespace) {
143
        return current().resourcesByNamespace.get(namespace);
×
144
    }
145

146
    /**
147
     * Find the maintained resources belonging to a given space.
148
     *
149
     * @param space The space to look up.
150
     * @return The list of maintained resources for the space, possibly empty.
151
     */
152
    public List<MaintainedResource> findResourcesBySpace(Space space) {
153
        List<MaintainedResource> l = current().resourcesBySpace.get(space);
×
154
        return l != null ? l : new ArrayList<>();
×
155
    }
156

157
    /**
158
     * Get a maintained resource by its id.
159
     *
160
     * @param id The id of the resource.
161
     * @return The corresponding MaintainedResource object, or null if not found.
162
     */
163
    public MaintainedResource findById(String id) {
164
        return current().resourcesById.get(id);
21✔
165
    }
166

167
    /**
168
     * Invalidate the underlying ApiCache entry, optionally delaying the next refresh.
169
     *
170
     * @param waitMillis Delay in milliseconds before the next access may trigger a refresh; 0 for immediate.
171
     */
172
    public void forceRootRefresh(long waitMillis) {
173
        ApiCache.clearCache(new QueryRef(QueryApiAccess.GET_MAINTAINED_RESOURCES), waitMillis);
×
174
    }
×
175

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