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

knowledgepixels / nanodash / 17915104384

22 Sep 2025 12:21PM UTC coverage: 13.525% (-0.2%) from 13.712%
17915104384

push

github

tkuhn
fix: Handling of failing queries

435 of 4046 branches covered (10.75%)

Branch coverage included in aggregate %.

1112 of 7392 relevant lines covered (15.04%)

0.67 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 java.util.HashMap;
4
import java.util.List;
5
import java.util.Map;
6
import java.util.Random;
7
import java.util.concurrent.ConcurrentHashMap;
8
import java.util.concurrent.ConcurrentMap;
9

10
import org.nanopub.extra.services.APINotReachableException;
11
import org.nanopub.extra.services.ApiResponse;
12
import org.nanopub.extra.services.ApiResponseEntry;
13
import org.nanopub.extra.services.FailedApiCallException;
14
import org.nanopub.extra.services.NotEnoughAPIInstancesException;
15
import org.nanopub.extra.services.QueryRef;
16
import org.slf4j.Logger;
17
import org.slf4j.LoggerFactory;
18

19
/**
20
 * A utility class for caching API responses and maps to reduce redundant API calls.
21
 * This class is thread-safe and ensures that cached data is refreshed periodically.
22
 */
23
public class ApiCache {
24

25
    private ApiCache() {
26
    } // no instances allowed
27

28
    private transient static ConcurrentMap<String, ApiResponse> cachedResponses = new ConcurrentHashMap<>();
×
29
    private transient static ConcurrentMap<String, Boolean> failed = new ConcurrentHashMap<>();
×
30
    private transient static ConcurrentMap<String, Map<String, String>> cachedMaps = new ConcurrentHashMap<>();
×
31
    private transient static ConcurrentMap<String, Long> lastRefresh = new ConcurrentHashMap<>();
×
32
    private transient static ConcurrentMap<String, Long> refreshStart = new ConcurrentHashMap<>();
×
33
    private static final Logger logger = LoggerFactory.getLogger(ApiCache.class);
×
34

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

46
    public static boolean isRunning(QueryRef queryRef) {
47
        return isRunning(queryRef.getAsUrlString());
×
48
    }
49
    /**
50
     * Updates the cached API response for a specific query reference.
51
     *
52
     * @param queryRef The query reference
53
     * @throws FailedApiCallException If the API call fails.
54
     */
55
    private static void updateResponse(QueryRef queryRef) throws FailedApiCallException, APINotReachableException, NotEnoughAPIInstancesException {
56
        ApiResponse response = QueryApiAccess.get(queryRef);
×
57
        String cacheId = queryRef.getAsUrlString();
×
58
        cachedResponses.put(cacheId, response);
×
59
        lastRefresh.put(cacheId, System.currentTimeMillis());
×
60
    }
×
61

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

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

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

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