• 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

67.53
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
import org.slf4j.Logger;
12
import org.slf4j.LoggerFactory;
13

14
import java.util.ArrayList;
15
import java.util.HashMap;
16
import java.util.List;
17
import java.util.Map;
18

19
public class MaintainedResourceRepository {
20

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

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

25
    private volatile List<MaintainedResource> resourceList;
26
    private Map<String, MaintainedResource> resourcesById;
27
    private Map<String, MaintainedResource> resourcesByNamespace;
28
    private Map<Space, List<MaintainedResource>> resourcesBySpace;
29
    private boolean loaded = false;
9✔
30
    private volatile Long runRootUpdateAfter = null;
9✔
31
    private final Object loadLock = new Object();
15✔
32

33
    /**
34
     * Get the singleton instance of MaintainedResourceRepository.
35
     *
36
     * @return The singleton instance of MaintainedResourceRepository.
37
     */
38
    public static MaintainedResourceRepository get() {
39
        return INSTANCE;
6✔
40
    }
41

42
    private MaintainedResourceRepository() {
6✔
43
    }
3✔
44

45
    /**
46
     * Refresh the list of maintained resources from the API response, updating the internal state accordingly.
47
     *
48
     * @param resp The API response containing maintained resource data.
49
     */
50
    public synchronized void refresh(ApiResponse resp) {
51
        List<MaintainedResource> newResourceList = new ArrayList<>();
12✔
52
        Map<String, MaintainedResource> newResourcesById = new HashMap<>();
12✔
53
        Map<Space, List<MaintainedResource>> newResourcesBySpace = new HashMap<>();
12✔
54
        Map<String, MaintainedResource> newResourcesByNamespace = new HashMap<>();
12✔
55
        for (ApiResponseEntry entry : resp.getData()) {
33✔
56
            Space space = SpaceRepository.get().findById(entry.get("space"));
18✔
57
            if (space == null) {
6!
58
                continue;
×
59
            }
60
            MaintainedResource resource = MaintainedResourceFactory.getOrCreate(entry, space);
12✔
61
            if (newResourcesById.containsKey(resource.getId())) {
15✔
62
                continue;
3✔
63
            }
64
            newResourceList.add(resource);
12✔
65
            newResourcesById.put(resource.getId(), resource);
18✔
66
            newResourcesBySpace.computeIfAbsent(space, k -> new ArrayList<>()).add(resource);
36✔
67
            if (resource.getNamespace() != null) {
9✔
68
                // TODO Handle conflicts when two resources claim the same namespace:
69
                newResourcesByNamespace.put(resource.getNamespace(), resource);
18✔
70
            }
71
        }
3✔
72
        MaintainedResourceFactory.removeStale(newResourcesById.keySet());
9✔
73
        resourcesById = newResourcesById;
9✔
74
        resourcesBySpace = newResourcesBySpace;
9✔
75
        resourcesByNamespace = newResourcesByNamespace;
9✔
76
        loaded = true;
9✔
77
        resourceList = newResourceList; // volatile write last — establishes happens-before for all above
9✔
78
    }
3✔
79

80
    /**
81
     * Find a maintained resource by its namespace.
82
     *
83
     * @param namespace The namespace to search for.
84
     * @return The MaintainedResource with the given namespace, or null if not found.
85
     */
86
    public MaintainedResource findByNamespace(String namespace) {
87
        return resourcesByNamespace.get(namespace);
×
88
    }
89

90
    /**
91
     * Find a maintained resource by its ID.
92
     *
93
     * @param space The space to which the resource belongs.
94
     * @return The MaintainedResource with the given ID, or null if not found.
95
     */
96
    public List<MaintainedResource> findResourcesBySpace(Space space) {
97
        return resourcesBySpace.computeIfAbsent(space, k -> new ArrayList<>());
×
98
    }
99

100
    /**
101
     * Get a maintained resource by its id.
102
     *
103
     * @param id The id of the resource.
104
     * @return The corresponding MaintainedResource object, or null if not found.
105
     */
106
    public MaintainedResource findById(String id) {
107
        ensureLoaded();
6✔
108
        return resourcesById.get(id);
18✔
109
    }
110

111
    /**
112
     * Ensure that the resources are loaded, fetching them from the API if necessary.
113
     */
114
    public void ensureLoaded() {
115
        if (resourceList == null) {
9✔
116
            try {
117
                synchronized (loadLock) {
15✔
118
                    if (runRootUpdateAfter != null) {
9!
119
                        while (runRootUpdateAfter != null && System.currentTimeMillis() < runRootUpdateAfter) {
×
120
                            Thread.sleep(100);
×
121
                        }
122
                        runRootUpdateAfter = null;
×
123
                    }
124
                }
9✔
125
            } catch (InterruptedException ex) {
×
126
                logger.error("Interrupted", ex);
×
127
            }
3✔
128
            if (resourceList == null) { // double-check after potential wait
9!
129
                refresh(ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_MAINTAINED_RESOURCES), true));
24✔
130
            }
131
        }
132
    }
3✔
133

134
    /**
135
     * Force a refresh of the maintained resources on the next access, with an optional delay to allow for updates to propagate.
136
     *
137
     * @param waitMillis Number of milliseconds to wait before allowing the next access to trigger a refresh. If 0, the refresh will happen immediately on the next access.
138
     */
139
    public void forceRootRefresh(long waitMillis) {
140
        resourceList = null;
×
141
        runRootUpdateAfter = System.currentTimeMillis() + waitMillis;
×
142
    }
×
143

144
    /**
145
     * Refresh the maintained resources by fetching the latest data from the API and updating the internal state accordingly.
146
     */
147
    public void refresh() {
148
        refresh(ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_MAINTAINED_RESOURCES), true));
×
149
        for (MaintainedResource resource : resourceList) {
×
150
            resource.setDataNeedsUpdate();
×
151
        }
×
152
    }
×
153

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