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

knowledgepixels / nanodash / 22733740442

05 Mar 2026 07:45PM UTC coverage: 15.868% (+0.06%) from 15.81%
22733740442

push

github

web-flow
Merge pull request #375 from knowledgepixels/fix/374-race-condition-maintained-resources

fix: Fix race conditions in repository refresh and Space field visibility

703 of 5385 branches covered (13.05%)

Branch coverage included in aggregate %.

1737 of 9992 relevant lines covered (17.38%)

2.37 hits per line

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

53.64
src/main/java/com/knowledgepixels/nanodash/repository/SpaceRepository.java
1
package com.knowledgepixels.nanodash.repository;
2

3
import com.github.jsonldjava.shaded.com.google.common.collect.Ordering;
4
import com.knowledgepixels.nanodash.ApiCache;
5
import com.knowledgepixels.nanodash.QueryApiAccess;
6
import com.knowledgepixels.nanodash.domain.Space;
7
import com.knowledgepixels.nanodash.domain.SpaceFactory;
8
import org.nanopub.extra.services.ApiResponse;
9
import org.nanopub.extra.services.ApiResponseEntry;
10
import org.nanopub.extra.services.QueryRef;
11
import org.slf4j.Logger;
12
import org.slf4j.LoggerFactory;
13

14
import java.util.*;
15

16
/**
17
 * Repository class for managing Space instances, providing methods to refresh, retrieve, and query spaces based on API responses.
18
 */
19
public class SpaceRepository {
20

21
    private static final Logger logger = LoggerFactory.getLogger(SpaceRepository.class);
9✔
22

23
    private static final SpaceRepository INSTANCE = new SpaceRepository();
15✔
24

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

34
    private volatile List<Space> spaceList;
35
    private Map<String, List<Space>> spaceListByType;
36
    private Map<String, Space> spacesById;
37
    private Map<Space, Set<Space>> subspaceMap;
38
    private Map<Space, Set<Space>> superspaceMap;
39
    private boolean loaded = false;
9✔
40
    private volatile Long runRootUpdateAfter = null;
9✔
41

42
    private final Object loadLock = new Object();
15✔
43

44
    private SpaceRepository() {
6✔
45
    }
3✔
46

47
    /**
48
     * Refresh the list of spaces from the API response.
49
     *
50
     * @param resp The API response containing space data.
51
     */
52
    public synchronized void refresh(ApiResponse resp) {
53
        logger.info("Refreshing spaces from API response with {} entries", resp.getData().size());
21✔
54
        List<Space> newSpaceList = new ArrayList<>();
12✔
55
        Map<String, List<Space>> newSpaceListByType = new HashMap<>();
12✔
56
        Map<String, Space> newSpacesById = new HashMap<>();
12✔
57
        Map<Space, Set<Space>> newSubspaceMap = new HashMap<>();
12✔
58
        Map<Space, Set<Space>> newSuperspaceMap = new HashMap<>();
12✔
59
        for (ApiResponseEntry entry : resp.getData()) {
33✔
60
            Space space;
61
            space = SpaceFactory.getOrCreate(entry);
9✔
62
            newSpaceList.add(space);
12✔
63
            newSpaceListByType.computeIfAbsent(space.getType(), k -> new ArrayList<>()).add(space);
39✔
64
            newSpacesById.put(space.getId(), space);
18✔
65
        }
3✔
66
        SpaceFactory.removeStale(newSpacesById.keySet());
9✔
67
        for (Space space : newSpaceList) {
30✔
68
            String id = space.getId();
9✔
69
            if (!id.matches("https?://[^/]+/.*/[^/]*/?")) continue;
12!
70
            String superId = id.replaceFirst("(https?://[^/]+/.*)/[^/]*/?", "$1");
15✔
71
            Space superSpace = newSpacesById.get(superId);
15✔
72
            if (superSpace == null) continue;
9✔
73
            newSubspaceMap.computeIfAbsent(superSpace, k -> new HashSet<>()).add(space);
36✔
74
            newSuperspaceMap.computeIfAbsent(space, k -> new HashSet<>()).add(superSpace);
36✔
75
            space.setDataNeedsUpdate();
6✔
76
        }
3✔
77
        spacesById = newSpacesById;
9✔
78
        spaceListByType = newSpaceListByType;
9✔
79
        subspaceMap = newSubspaceMap;
9✔
80
        superspaceMap = newSuperspaceMap;
9✔
81
        loaded = true;
9✔
82
        spaceList = newSpaceList; // volatile write last — establishes happens-before for all above
9✔
83
    }
3✔
84

85
    /**
86
     * Force a refresh of the root spaces after a specified delay, allowing for any ongoing updates to complete before the next refresh.
87
     *
88
     * @param waitMillis The number of milliseconds to wait before allowing the next refresh to occur.
89
     */
90
    public void forceRootRefresh(long waitMillis) {
91
        spaceList = null;
×
92
        runRootUpdateAfter = System.currentTimeMillis() + waitMillis;
×
93
    }
×
94

95
    /**
96
     * Ensure that the spaces are loaded, fetching them from the API if necessary.
97
     */
98
    public void ensureLoaded() {
99
        if (spaceList == null) {
9✔
100
            try {
101
                synchronized (loadLock) {
15✔
102
                    if (runRootUpdateAfter != null) {
9!
103
                        while (System.currentTimeMillis() < runRootUpdateAfter) {
×
104
                            Thread.sleep(100);
×
105
                        }
106
                        runRootUpdateAfter = null;
×
107
                    }
108
                }
9✔
109
            } catch (InterruptedException ex) {
×
110
                logger.error("Interrupted", ex);
×
111
            }
3✔
112
            if (spaceList == null) { // double-check after potential wait
9!
113
                refresh(ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_SPACES), true));
24✔
114
            }
115
        }
116
    }
3✔
117

118
    /**
119
     * Get a space by its id.
120
     *
121
     * @param id The id of the space.
122
     * @return The corresponding Space object, or null if not found.
123
     */
124
    public Space findById(String id) {
125
        ensureLoaded();
6✔
126
        return spacesById.get(id);
18✔
127
    }
128

129
    /**
130
     * Get spaces by their type.
131
     *
132
     * @param type The type of spaces to retrieve.
133
     * @return List of Space objects matching the specified type, or an empty list if none are found.
134
     */
135
    public List<Space> findByType(String type) {
136
        ensureLoaded();
×
137
        return spaceListByType.computeIfAbsent(type, k -> new ArrayList<>());
×
138
    }
139

140
    /**
141
     * Get subspaces of a given space.
142
     *
143
     * @param space The space for which to find subspaces.
144
     * @return List of subspaces.
145
     */
146
    public List<Space> findSubspaces(Space space) {
147
        if (subspaceMap.containsKey(space)) {
×
148
            List<Space> subspaces = new ArrayList<>(subspaceMap.get(space));
×
149
            subspaces.sort(Ordering.usingToString());
×
150
            return subspaces;
×
151
        }
152
        return new ArrayList<>();
×
153
    }
154

155
    /**
156
     * Get subspaces of a given space that match a specific type.
157
     *
158
     * @param space The space for which to find subspaces.
159
     * @param type  The type of subspaces to filter by.
160
     * @return List of subspaces matching the specified type.
161
     */
162
    public List<Space> findSubspaces(Space space, String type) {
163
        List<Space> l = new ArrayList<>();
×
164
        for (Space s : findSubspaces(space)) {
×
165
            if (s.getType().equals(type)) l.add(s);
×
166
        }
×
167
        return l;
×
168
    }
169

170
    /**
171
     * Get superspaces of this space.
172
     *
173
     * @return List of superspaces.
174
     */
175
    public List<Space> findSuperspaces(Space space) {
176
        if (superspaceMap.containsKey(space)) {
×
177
            List<Space> superspaces = new ArrayList<>(superspaceMap.get(space));
×
178
            superspaces.sort(Ordering.usingToString());
×
179
            return superspaces;
×
180
        }
181
        return new ArrayList<>();
×
182
    }
183

184
    /**
185
     * Mark all spaces as needing a data update.
186
     */
187
    public void refresh() {
188
        refresh(ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_SPACES), true));
×
189
        for (Space space : spaceList) {
×
190
            space.setDataNeedsUpdate();
×
191
        }
×
192
    }
×
193

194
    private Space getIdSuperspace(Space space) {
195
        String id = space.getId();
×
196
        if (!id.matches("https?://[^/]+/.*/[^/]*/?")) return null;
×
197
        String superId = id.replaceFirst("(https?://[^/]+/.*)/[^/]*/?", "$1");
×
198
        return spacesById.get(superId);
×
199
    }
200

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