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

knowledgepixels / nanodash / 22177550615

19 Feb 2026 10:13AM UTC coverage: 14.25% (+0.6%) from 13.613%
22177550615

push

github

ashleycaselli
test(ApiCache): add unit tests for `retrieveResponseSync` method and minor changes

596 of 5284 branches covered (11.28%)

Branch coverage included in aggregate %.

1540 of 9705 relevant lines covered (15.87%)

2.07 hits per line

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

51.46
src/main/java/com/knowledgepixels/nanodash/ApiCache.java
1
package com.knowledgepixels.nanodash;
2

3
import org.nanopub.extra.services.*;
4
import org.slf4j.Logger;
5
import org.slf4j.LoggerFactory;
6

7
import java.util.HashMap;
8
import java.util.List;
9
import java.util.Map;
10
import java.util.Random;
11
import java.util.concurrent.ConcurrentHashMap;
12
import java.util.concurrent.ConcurrentMap;
13

14
/**
15
 * A utility class for caching API responses and maps to reduce redundant API calls.
16
 * This class is thread-safe and ensures that cached data is refreshed periodically.
17
 */
18
public class ApiCache {
19

20
    private ApiCache() {
21
    } // no instances allowed
22

23
    private transient static ConcurrentMap<String, ApiResponse> cachedResponses = new ConcurrentHashMap<>();
12✔
24
    private transient static ConcurrentMap<String, Integer> failed = new ConcurrentHashMap<>();
12✔
25
    private transient static ConcurrentMap<String, Map<String, String>> cachedMaps = new ConcurrentHashMap<>();
12✔
26
    private transient static ConcurrentMap<String, Long> lastRefresh = new ConcurrentHashMap<>();
12✔
27
    private transient static ConcurrentMap<String, Long> refreshStart = new ConcurrentHashMap<>();
12✔
28
    private transient static ConcurrentMap<String, Long> runAfter = new ConcurrentHashMap<>();
12✔
29
    private static final Logger logger = LoggerFactory.getLogger(ApiCache.class);
12✔
30

31
    /**
32
     * Checks if a cache refresh is currently running for the given cache ID.
33
     *
34
     * @param cacheId The unique identifier for the cache.
35
     * @return True if a refresh is running, false otherwise.
36
     */
37
    private static boolean isRunning(String cacheId) {
38
        if (!refreshStart.containsKey(cacheId)) return false;
18✔
39
        return System.currentTimeMillis() - refreshStart.get(cacheId) < 60 * 1000;
42✔
40
    }
41

42
    /**
43
     * Checks if a cache refresh is currently running for the given QueryRef.
44
     *
45
     * @param queryRef The query reference
46
     * @return True if a refresh is running, false otherwise.
47
     */
48
    public static boolean isRunning(QueryRef queryRef) {
49
        return isRunning(queryRef.getAsUrlString());
12✔
50
    }
51

52
    /**
53
     * Updates the cached API response for a specific query reference.
54
     *
55
     * @param queryRef The query reference
56
     * @throws FailedApiCallException If the API call fails.
57
     */
58
    private static void updateResponse(QueryRef queryRef, boolean forced) throws FailedApiCallException, APINotReachableException, NotEnoughAPIInstancesException {
59
        ApiResponse response;
60
        if (forced) {
6✔
61
            response = QueryApiAccess.forcedGet(queryRef);
12✔
62
        } else {
63
            response = QueryApiAccess.get(queryRef);
9✔
64
        }
65
        String cacheId = queryRef.getAsUrlString();
9✔
66
        logger.info("Updating cached API response for {}", cacheId);
12✔
67
        cachedResponses.put(cacheId, response);
15✔
68
        lastRefresh.put(cacheId, System.currentTimeMillis());
18✔
69
    }
3✔
70

71
    public static ApiResponse retrieveResponseSync(QueryRef queryRef, boolean forced) {
72
        long timeNow = System.currentTimeMillis();
6✔
73
        String cacheId = queryRef.getAsUrlString();
9✔
74
        logger.info("Retrieving cached API response synchronously for {}", cacheId);
12✔
75
        boolean needsRefresh = true;
6✔
76
        if (cachedResponses.containsKey(cacheId) && cachedResponses.get(cacheId) != null) {
24!
77
            long cacheAge = timeNow - lastRefresh.get(cacheId);
24✔
78
            needsRefresh = cacheAge > 60 * 1000;
24✔
79
        }
80
        if (failed.get(cacheId) != null && failed.get(cacheId) > 2) {
33!
81
            failed.remove(cacheId);
12✔
82
            throw new RuntimeException("Query failed: " + cacheId);
18✔
83
        }
84
        if (needsRefresh && !isRunning(cacheId)) {
15✔
85
            logger.info("Refreshing cache for {}", cacheId);
12✔
86
            refreshStart.put(cacheId, timeNow);
18✔
87
            try {
88
                if (runAfter.containsKey(cacheId)) {
12!
89
                    while (System.currentTimeMillis() < runAfter.get(cacheId)) {
×
90
                        Thread.sleep(100);
×
91
                    }
92
                    runAfter.remove(cacheId);
×
93
                }
94
                if (failed.get(cacheId) != null) {
12!
95
                    // 1 second pause between failed attempts;
96
                    Thread.sleep(1000);
×
97
                }
98
                Thread.sleep(100 + new Random().nextLong(400));
24✔
99
            } catch (InterruptedException ex) {
×
100
                logger.error("Interrupted while waiting to refresh cache: {}", ex.getMessage());
×
101
            }
3✔
102
            try {
103
                ApiCache.updateResponse(queryRef, forced);
9✔
104
            } catch (Exception ex) {
3✔
105
                logger.error("Failed to update cache for {}: {}", cacheId, ex.getMessage());
18✔
106
                cachedResponses.remove(cacheId);
12✔
107
                failed.merge(cacheId, 1, Integer::sum);
21✔
108
                lastRefresh.put(cacheId, System.currentTimeMillis());
18✔
109
            } finally {
110
                refreshStart.remove(cacheId);
12✔
111
            }
112
        }
113
        return cachedResponses.getOrDefault(cacheId, null);
18✔
114
    }
115

116
    /**
117
     * Retrieves a cached API response for a specific QueryRef.
118
     *
119
     * @param queryRef The QueryRef object containing the query name and parameters.
120
     * @return The cached API response, or null if not cached.
121
     */
122
    public static ApiResponse retrieveResponseAsync(QueryRef queryRef) {
123
        long timeNow = System.currentTimeMillis();
6✔
124
        String cacheId = queryRef.getAsUrlString();
9✔
125
        logger.info("Retrieving cached API response asynchronously for {}", cacheId);
12✔
126
        boolean isCached = false;
6✔
127
        boolean needsRefresh = true;
6✔
128
        if (cachedResponses.containsKey(cacheId) && cachedResponses.get(cacheId) != null) {
24!
129
            long cacheAge = timeNow - lastRefresh.get(cacheId);
24✔
130
            isCached = cacheAge < 24 * 60 * 60 * 1000;
21!
131
            needsRefresh = cacheAge > 60 * 1000;
18!
132
        }
133
        if (failed.get(cacheId) != null && failed.get(cacheId) > 2) {
12!
134
            failed.remove(cacheId);
×
135
            throw new RuntimeException("Query failed: " + cacheId);
×
136
        }
137
        if (needsRefresh && !isRunning(cacheId)) {
15!
138
            refreshStart.put(cacheId, timeNow);
18✔
139
            new Thread(() -> {
18✔
140
                try {
141
                    if (runAfter.containsKey(cacheId)) {
12!
142
                        while (System.currentTimeMillis() < runAfter.get(cacheId)) {
×
143
                            Thread.sleep(100);
×
144
                        }
145
                        runAfter.remove(cacheId);
×
146
                    }
147
                    if (failed.get(cacheId) != null) {
12!
148
                        // 1 second pause between failed attempts;
149
                        Thread.sleep(1000);
×
150
                    }
151
                    Thread.sleep(100 + new Random().nextLong(400));
24✔
152
                } catch (InterruptedException ex) {
×
153
                    logger.error("Interrupted while waiting to refresh cache: {}", ex.getMessage());
×
154
                }
3✔
155
                try {
156
                    ApiCache.updateResponse(queryRef, false);
9✔
157
                } catch (Exception ex) {
×
158
                    logger.error("Failed to update cache for {}: {}", cacheId, ex.getMessage());
×
159
                    cachedResponses.remove(cacheId);
×
160
                    failed.merge(cacheId, 1, Integer::sum);
×
161
                    lastRefresh.put(cacheId, System.currentTimeMillis());
×
162
                } finally {
163
                    refreshStart.remove(cacheId);
12✔
164
                }
165
            }).start();
6✔
166
        }
167
        if (isCached) {
6✔
168
            return cachedResponses.get(cacheId);
15✔
169
        } else {
170
            return null;
6✔
171
        }
172
    }
173

174
    /**
175
     * Updates the cached map for a specific query reference.
176
     *
177
     * @param queryRef The query reference
178
     * @throws FailedApiCallException If the API call fails.
179
     */
180
    private static void updateMap(QueryRef queryRef) throws FailedApiCallException, APINotReachableException, NotEnoughAPIInstancesException {
181
        Map<String, String> map = new HashMap<>();
×
182
        List<ApiResponseEntry> respList = QueryApiAccess.get(queryRef).getData();
×
183
        while (respList != null && !respList.isEmpty()) {
×
184
            ApiResponseEntry resultEntry = respList.removeFirst();
×
185
            map.put(resultEntry.get("key"), resultEntry.get("value"));
×
186
        }
×
187
        String cacheId = queryRef.getAsUrlString();
×
188
        cachedMaps.put(cacheId, map);
×
189
        lastRefresh.put(cacheId, System.currentTimeMillis());
×
190
    }
×
191

192
    /**
193
     * Retrieves a cached map for a specific query reference.
194
     * If the cache is stale, it triggers a background refresh.
195
     *
196
     * @param queryRef The query reference
197
     * @return The cached map, or null if not cached.
198
     */
199
    public static synchronized Map<String, String> retrieveMap(QueryRef queryRef) {
200
        long timeNow = System.currentTimeMillis();
×
201
        String cacheId = queryRef.getAsUrlString();
×
202
        boolean isCached = false;
×
203
        boolean needsRefresh = true;
×
204
        if (cachedMaps.containsKey(cacheId)) {
×
205
            long cacheAge = timeNow - lastRefresh.get(cacheId);
×
206
            isCached = cacheAge < 24 * 60 * 60 * 1000;
×
207
            needsRefresh = cacheAge > 60 * 1000;
×
208
        }
209
        if (needsRefresh && !isRunning(cacheId)) {
×
210
            refreshStart.put(cacheId, timeNow);
×
211
            new Thread(() -> {
×
212
                try {
213
                    if (runAfter.containsKey(cacheId)) {
×
214
                        while (System.currentTimeMillis() < runAfter.get(cacheId)) {
×
215
                            Thread.sleep(100);
×
216
                        }
217
                        runAfter.remove(cacheId);
×
218
                    }
219
                    Thread.sleep(100 + new Random().nextLong(400));
×
220
                } catch (InterruptedException ex) {
×
221
                    logger.error("Interrupted while waiting to refresh cache: {}", ex.getMessage());
×
222
                }
×
223
                try {
224
                    ApiCache.updateMap(queryRef);
×
225
                } catch (Exception ex) {
×
226
                    logger.error("Failed to update cache for {}: {}", cacheId, ex.getMessage());
×
227
                    cachedResponses.put(cacheId, null);
×
228
                    lastRefresh.put(cacheId, System.currentTimeMillis());
×
229
                } finally {
230
                    refreshStart.remove(cacheId);
×
231
                }
232
            }).start();
×
233
        }
234
        if (isCached) {
×
235
            if (cachedResponses.get(cacheId) == null) {
×
236
                cachedResponses.remove(cacheId);
×
237
                throw new RuntimeException("Query failed: " + cacheId);
×
238
            }
239
            return cachedMaps.get(cacheId);
×
240
        } else {
241
            return null;
×
242
        }
243
    }
244

245
    /**
246
     * Clears the cached response for a specific query reference and sets a delay before the next refresh can occur.
247
     *
248
     * @param queryRef   The query reference for which to clear the cache.
249
     * @param waitMillis The amount of time in milliseconds to wait before allowing the cache to be refreshed again.
250
     */
251
    public static void clearCache(QueryRef queryRef, long waitMillis) {
252
        if (waitMillis < 0) {
12✔
253
            throw new IllegalArgumentException("waitMillis must be non-negative");
15✔
254
        }
255
        cachedResponses.remove(queryRef.getAsUrlString());
15✔
256
        runAfter.put(queryRef.getAsUrlString(), System.currentTimeMillis() + waitMillis);
27✔
257
    }
3✔
258

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