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

Nanopublication / nanopub-java / 23729181399

30 Mar 2026 05:19AM UTC coverage: 51.353% (+0.01%) from 51.341%
23729181399

push

github

tkuhn
fix: prevent HTTP connection pool exhaustion under concurrent load

Increase connectionRequestTimeout from 500ms to 10s to match the other
timeouts — 500ms was too aggressive for a shared pool under concurrent
load, causing "Timeout waiting for connection from pool" errors
(knowledgepixels/nanodash#416).

Also fix two connection leak paths: consume response entities in
QueryCall.getApiInstances() health checks, and ensure QueryAccess.call()
always releases connections via try-with-resources and a finally block.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

1030 of 2990 branches covered (34.45%)

Branch coverage included in aggregate %.

5253 of 9245 relevant lines covered (56.82%)

7.98 hits per line

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

73.33
src/main/java/org/nanopub/extra/services/QueryAccess.java
1
package org.nanopub.extra.services;
2

3
import com.opencsv.CSVParserBuilder;
4
import com.opencsv.CSVReader;
5
import com.opencsv.CSVReaderBuilder;
6
import com.opencsv.CSVWriterBuilder;
7
import com.opencsv.ICSVParser;
8
import com.opencsv.ICSVWriter;
9
import com.opencsv.exceptions.CsvValidationException;
10
import org.apache.http.HttpResponse;
11
import org.apache.http.util.EntityUtils;
12
import org.eclipse.rdf4j.model.Model;
13
import org.eclipse.rdf4j.rio.RDFFormat;
14
import org.eclipse.rdf4j.rio.Rio;
15

16
import java.io.BufferedReader;
17
import java.io.IOException;
18
import java.io.InputStream;
19
import java.io.InputStreamReader;
20
import java.io.Writer;
21

22
/**
23
 * Second-generation query API access
24
 */
25
public abstract class QueryAccess {
9✔
26

27
    /**
28
     * Process the header.
29
     *
30
     * @param line the header line from the CSV response
31
     */
32
    protected abstract void processHeader(String[] line);
33

34
    /**
35
     * Process a line of data from the CSV response.
36
     *
37
     * @param line the line of data from the CSV response
38
     */
39
    protected abstract void processLine(String[] line);
40

41
    /**
42
     * Process the RDF content from a SPARQL CONSTRUCT query response.
43
     * Override this method to handle RDF responses. Default implementation is a no-op.
44
     *
45
     * @param model the parsed RDF model
46
     */
47
    protected void processRdfContent(Model model) {
48
    }
×
49

50
    /**
51
     * Call a query with the given queryId and parameters.
52
     *
53
     * @param queryRef the query reference
54
     * @throws FailedApiCallException         if the API call fails
55
     * @throws APINotReachableException       if the API is not reachable
56
     * @throws NotEnoughAPIInstancesException if there are not enough API instances available
57
     */
58
    public void call(QueryRef queryRef) throws FailedApiCallException, APINotReachableException, NotEnoughAPIInstancesException {
59
        HttpResponse resp = QueryCall.run(queryRef);
9✔
60
        try {
61
            String contentType = resp.getFirstHeader("Content-Type") != null
12!
62
                    ? resp.getFirstHeader("Content-Type").getValue() : "";
18✔
63
            String mimeType = contentType.contains(";") ? contentType.split(";")[0].trim() : contentType.trim();
42✔
64
            if (mimeType.equals("text/csv") || mimeType.isEmpty()) {
21!
65
                try (CSVReader csvReader = new CSVReaderBuilder(new BufferedReader(new InputStreamReader(resp.getEntity().getContent())))
48✔
66
                        .withCSVParser(new CSVParserBuilder().withEscapeChar(ICSVParser.NULL_CHARACTER).build())
9✔
67
                        .build()) {
6✔
68
                    String[] line = null;
6✔
69
                    int n = 0;
6✔
70
                    while ((line = csvReader.readNext()) != null) {
15✔
71
                        n++;
3✔
72
                        if (n == 1) {
9✔
73
                            processHeader(line);
12✔
74
                        } else {
75
                            processLine(line);
12✔
76
                        }
77
                    }
78
                } catch (IOException | CsvValidationException ex) {
×
79
                    throw new FailedApiCallException(ex);
×
80
                }
3✔
81
            } else {
82
                RDFFormat format = Rio.getParserFormatForMIMEType(mimeType).orElse(RDFFormat.TURTLE);
18✔
83
                try (InputStream in = resp.getEntity().getContent()) {
12✔
84
                    Model model = Rio.parse(in, format);
18✔
85
                    processRdfContent(model);
9✔
86
                } catch (IOException ex) {
×
87
                    throw new FailedApiCallException(ex);
×
88
                }
3✔
89
            }
90
        } finally {
91
            EntityUtils.consumeQuietly(resp.getEntity());
9✔
92
        }
93
    }
3✔
94

95
    /**
96
     * Print the response of a query in CSV format to the given writer.
97
     *
98
     * @param queryRef the query reference
99
     * @param writer   the writer to print the CSV response to
100
     * @throws FailedApiCallException         if the API call fails
101
     * @throws APINotReachableException       if the API is not reachable
102
     * @throws NotEnoughAPIInstancesException if there are not enough API instances available
103
     */
104
    public static void printCvsResponse(QueryRef queryRef, Writer writer) throws FailedApiCallException, APINotReachableException, NotEnoughAPIInstancesException {
105
        ICSVWriter icsvWriter = new CSVWriterBuilder(writer).withSeparator(',').build();
×
106
        QueryAccess a = new QueryAccess() {
×
107

108
            @Override
109
            protected void processHeader(String[] line) {
110
                icsvWriter.writeNext(line);
×
111
            }
×
112

113
            @Override
114
            protected void processLine(String[] line) {
115
                icsvWriter.writeNext(line);
×
116
            }
×
117

118
        };
119
        a.call(queryRef);
×
120
        icsvWriter.flushQuietly();
×
121
    }
×
122

123
    /**
124
     * Get the response of a query as an ApiResponse object.
125
     *
126
     * @param queryRef the query reference
127
     * @return an ApiResponse object containing the response data
128
     * @throws FailedApiCallException         if the API call fails
129
     * @throws APINotReachableException       if the API is not reachable
130
     * @throws NotEnoughAPIInstancesException if there are not enough API instances available
131
     */
132
    public static ApiResponse get(QueryRef queryRef) throws FailedApiCallException, APINotReachableException, NotEnoughAPIInstancesException {
133
        final ApiResponse response = new ApiResponse();
12✔
134
        QueryAccess a = new QueryAccess() {
33✔
135

136
            @Override
137
            protected void processHeader(String[] line) {
138
                response.setHeader(line);
12✔
139
            }
3✔
140

141
            @Override
142
            protected void processLine(String[] line) {
143
                response.add(line);
12✔
144
            }
3✔
145

146
            @Override
147
            protected void processRdfContent(Model model) {
148
                response.setRdfContent(model);
12✔
149
            }
3✔
150

151
        };
152
        a.call(queryRef);
9✔
153
        return response;
6✔
154
    }
155

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