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

knowledgepixels / nanodash / 20299457275

17 Dec 2025 10:20AM UTC coverage: 14.401% (-0.9%) from 15.279%
20299457275

push

github

tkuhn
fix: Use API result cache for all requests

546 of 5004 branches covered (10.91%)

Branch coverage included in aggregate %.

1496 of 9176 relevant lines covered (16.3%)

2.13 hits per line

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

41.12
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;
39!
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
        cachedResponses.put(cacheId, response);
15✔
67
        lastRefresh.put(cacheId, System.currentTimeMillis());
18✔
68
    }
3✔
69

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

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

184
    /**
185
     * Updates the cached map for a specific query reference.
186
     *
187
     * @param queryRef The query reference
188
     * @throws FailedApiCallException If the API call fails.
189
     */
190
    private static void updateMap(QueryRef queryRef) throws FailedApiCallException, APINotReachableException, NotEnoughAPIInstancesException {
191
        Map<String, String> map = new HashMap<>();
×
192
        List<ApiResponseEntry> respList = QueryApiAccess.get(queryRef).getData();
×
193
        while (respList != null && !respList.isEmpty()) {
×
194
            ApiResponseEntry resultEntry = respList.remove(0);
×
195
            map.put(resultEntry.get("key"), resultEntry.get("value"));
×
196
        }
×
197
        String cacheId = queryRef.getAsUrlString();
×
198
        cachedMaps.put(cacheId, map);
×
199
        lastRefresh.put(cacheId, System.currentTimeMillis());
×
200
    }
×
201

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

255
    public static void clearCache(QueryRef queryRef, long waitMillis) {
256
        cachedResponses.remove(queryRef.getAsUrlString());
×
257
        runAfter.put(queryRef.getAsUrlString(), System.currentTimeMillis() + waitMillis);
×
258
    }
×
259

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