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

knowledgepixels / nanodash / 19367982417

14 Nov 2025 02:41PM UTC coverage: 13.676% (-0.6%) from 14.317%
19367982417

push

github

tkuhn
fix: Re-trying failed queries 2 times to reduce query failure errors

520 of 4782 branches covered (10.87%)

Branch coverage included in aggregate %.

1341 of 8826 relevant lines covered (15.19%)

0.68 hits per line

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

0.0
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<>();
×
24
    private transient static ConcurrentMap<String, Integer> failed = new ConcurrentHashMap<>();
×
25
    private transient static ConcurrentMap<String, Map<String, String>> cachedMaps = new ConcurrentHashMap<>();
×
26
    private transient static ConcurrentMap<String, Long> lastRefresh = new ConcurrentHashMap<>();
×
27
    private transient static ConcurrentMap<String, Long> refreshStart = new ConcurrentHashMap<>();
×
28
    private transient static ConcurrentMap<String, Long> runAfter = new ConcurrentHashMap<>();
×
29
    private static final Logger logger = LoggerFactory.getLogger(ApiCache.class);
×
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;
×
39
        return System.currentTimeMillis() - refreshStart.get(cacheId) < 60 * 1000;
×
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());
×
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) throws FailedApiCallException, APINotReachableException, NotEnoughAPIInstancesException {
59
        ApiResponse response = QueryApiAccess.get(queryRef);
×
60
        String cacheId = queryRef.getAsUrlString();
×
61
        cachedResponses.put(cacheId, response);
×
62
        lastRefresh.put(cacheId, System.currentTimeMillis());
×
63
    }
×
64

65
    /**
66
     * Retrieves a cached API response for a specific QueryRef.
67
     *
68
     * @param queryRef The QueryRef object containing the query name and parameters.
69
     * @return The cached API response, or null if not cached.
70
     */
71
    public static ApiResponse retrieveResponse(QueryRef queryRef) {
72
        long timeNow = System.currentTimeMillis();
×
73
        String cacheId = queryRef.getAsUrlString();
×
74
        boolean isCached = false;
×
75
        boolean needsRefresh = true;
×
76
        if (cachedResponses.containsKey(cacheId) && cachedResponses.get(cacheId) != null) {
×
77
            long cacheAge = timeNow - lastRefresh.get(cacheId);
×
78
            isCached = cacheAge < 24 * 60 * 60 * 1000;
×
79
            needsRefresh = cacheAge > 60 * 1000;
×
80
        }
81
        if (failed.get(cacheId) != null && failed.get(cacheId) > 2) {
×
82
            failed.remove(cacheId);
×
83
            throw new RuntimeException("Query failed: " + cacheId);
×
84
        }
85
        if (needsRefresh && !isRunning(cacheId)) {
×
86
            refreshStart.put(cacheId, timeNow);
×
87
            new Thread(() -> {
×
88
                try {
89
                    if (runAfter.containsKey(cacheId)) {
×
90
                        while (System.currentTimeMillis() < runAfter.get(cacheId)) {
×
91
                            Thread.sleep(100);
×
92
                        }
93
                        runAfter.remove(cacheId);
×
94
                    }
95
                    if (failed.get(cacheId) != null) {
×
96
                        // 1 second pause between failed attempts;
97
                        Thread.sleep(1000);
×
98
                    }
99
                    Thread.sleep(100 + new Random().nextLong(400));
×
100
                } catch (InterruptedException ex) {
×
101
                    logger.error("Interrupted while waiting to refresh cache: {}", ex.getMessage());
×
102
                }
×
103
                try {
104
                    ApiCache.updateResponse(queryRef);
×
105
                } catch (Exception ex) {
×
106
                    logger.error("Failed to update cache for {}: {}", cacheId, ex.getMessage());
×
107
                    cachedResponses.remove(cacheId);
×
108
                    if (failed.get(cacheId) == null) {
×
109
                        failed.put(cacheId, 1);
×
110
                    } else {
111
                        failed.put(cacheId, failed.get(cacheId) + 1);
×
112
                    }
113
                    lastRefresh.put(cacheId, System.currentTimeMillis());
×
114
                } finally {
115
                    refreshStart.remove(cacheId);
×
116
                }
117
            }).start();
×
118
        }
119
        if (isCached) {
×
120
            return cachedResponses.get(cacheId);
×
121
        } else {
122
            return null;
×
123
        }
124
    }
125

126
    /**
127
     * Updates the cached map for a specific query reference.
128
     *
129
     * @param queryRef The query reference
130
     * @throws FailedApiCallException If the API call fails.
131
     */
132
    private static void updateMap(QueryRef queryRef) throws FailedApiCallException, APINotReachableException, NotEnoughAPIInstancesException {
133
        Map<String, String> map = new HashMap<>();
×
134
        List<ApiResponseEntry> respList = QueryApiAccess.get(queryRef).getData();
×
135
        while (respList != null && !respList.isEmpty()) {
×
136
            ApiResponseEntry resultEntry = respList.remove(0);
×
137
            map.put(resultEntry.get("key"), resultEntry.get("value"));
×
138
        }
×
139
        String cacheId = queryRef.getAsUrlString();
×
140
        cachedMaps.put(cacheId, map);
×
141
        lastRefresh.put(cacheId, System.currentTimeMillis());
×
142
    }
×
143

144
    /**
145
     * Retrieves a cached map for a specific query reference.
146
     * If the cache is stale, it triggers a background refresh.
147
     *
148
     * @param queryRef The query reference
149
     * @return The cached map, or null if not cached.
150
     */
151
    public static synchronized Map<String, String> retrieveMap(QueryRef queryRef) {
152
        long timeNow = System.currentTimeMillis();
×
153
        String cacheId = queryRef.getAsUrlString();
×
154
        boolean isCached = false;
×
155
        boolean needsRefresh = true;
×
156
        if (cachedMaps.containsKey(cacheId)) {
×
157
            long cacheAge = timeNow - lastRefresh.get(cacheId);
×
158
            isCached = cacheAge < 24 * 60 * 60 * 1000;
×
159
            needsRefresh = cacheAge > 60 * 1000;
×
160
        }
161
        if (needsRefresh && !isRunning(cacheId)) {
×
162
            refreshStart.put(cacheId, timeNow);
×
163
            new Thread(() -> {
×
164
                try {
165
                    if (runAfter.containsKey(cacheId)) {
×
166
                        while (System.currentTimeMillis() < runAfter.get(cacheId)) {
×
167
                            Thread.sleep(100);
×
168
                        }
169
                        runAfter.remove(cacheId);
×
170
                    }
171
                    Thread.sleep(100 + new Random().nextLong(400));
×
172
                } catch (InterruptedException ex) {
×
173
                    logger.error("Interrupted while waiting to refresh cache: {}", ex.getMessage());
×
174
                }
×
175
                try {
176
                    ApiCache.updateMap(queryRef);
×
177
                } catch (Exception ex) {
×
178
                    logger.error("Failed to update cache for {}: {}", cacheId, ex.getMessage());
×
179
                    cachedResponses.put(cacheId, null);
×
180
                    lastRefresh.put(cacheId, System.currentTimeMillis());
×
181
                } finally {
182
                    refreshStart.remove(cacheId);
×
183
                }
184
            }).start();
×
185
        }
186
        if (isCached) {
×
187
            if (cachedResponses.get(cacheId) == null) {
×
188
                cachedResponses.remove(cacheId);
×
189
                throw new RuntimeException("Query failed: " + cacheId);
×
190
            }
191
            return cachedMaps.get(cacheId);
×
192
        } else {
193
            return null;
×
194
        }
195
    }
196

197
    public static void clearCache(QueryRef queryRef, long waitMillis) {
198
        cachedResponses.remove(queryRef.getAsUrlString());
×
199
        runAfter.put(queryRef.getAsUrlString(), System.currentTimeMillis() + waitMillis);
×
200
    }
×
201

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