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

knowledgepixels / nanodash / 19366908152

14 Nov 2025 02:02PM UTC coverage: 14.317% (+0.4%) from 13.87%
19366908152

push

github

tkuhn
feat: "refresh-upon-publish" PublishPage param to force cache refreshing

546 of 4776 branches covered (11.43%)

Branch coverage included in aggregate %.

1401 of 8823 relevant lines covered (15.88%)

0.71 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, Boolean> 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) {
×
82
            cachedResponses.remove(cacheId);
×
83
            failed.remove(cacheId);
×
84
            throw new RuntimeException("Query failed: " + cacheId);
×
85
        }
86
        if (needsRefresh && !isRunning(cacheId)) {
×
87
            refreshStart.put(cacheId, timeNow);
×
88
            new Thread(() -> {
×
89
                try {
90
                    if (runAfter.containsKey(cacheId)) {
×
91
                        while (System.currentTimeMillis() < runAfter.get(cacheId)) {
×
92
                            Thread.sleep(100);
×
93
                        }
94
                        runAfter.remove(cacheId);
×
95
                    }
96
                    Thread.sleep(100 + new Random().nextLong(400));
×
97
                } catch (InterruptedException ex) {
×
98
                    logger.error("Interrupted while waiting to refresh cache: {}", ex.getMessage());
×
99
                }
×
100
                try {
101
                    ApiCache.updateResponse(queryRef);
×
102
                } catch (Exception ex) {
×
103
                    logger.error("Failed to update cache for {}: {}", cacheId, ex.getMessage());
×
104
                    cachedResponses.remove(cacheId);
×
105
                    failed.put(cacheId, true);
×
106
                    lastRefresh.put(cacheId, System.currentTimeMillis());
×
107
                } finally {
108
                    refreshStart.remove(cacheId);
×
109
                }
110
            }).start();
×
111
        }
112
        if (isCached) {
×
113
            return cachedResponses.get(cacheId);
×
114
        } else {
115
            return null;
×
116
        }
117
    }
118

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

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

190
    public static void clearCache(QueryRef queryRef, long waitMillis) {
191
        cachedResponses.remove(queryRef.getAsUrlString());
×
192
        runAfter.put(queryRef.getAsUrlString(), System.currentTimeMillis() + waitMillis);
×
193
    }
×
194

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