Skip to content

Commit 1c25812

Browse files
committed
asynchronous API
make API calls to set configuration asynchronous fixes #3678 fixes #3686 fixes #3771
1 parent d6df19e commit 1c25812

File tree

21 files changed

+1158
-86
lines changed

21 files changed

+1158
-86
lines changed

apiary.apib

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ Besides `/suggester`, `/search` and `/metrics` endpoints, everything is accessib
88
unless authentication bearer tokens are configured in the web application and used via the 'Authorization' HTTP header
99
(within HTTPS connection).
1010

11+
Some of the APIs are asynchronous. They return status code 202 (Accepted) and a Location header that contains the URL
12+
for the status endpoint to check for the result of the API call. Once the status API returns result other than 202,
13+
the client should issue DELETE request to this URL to clean up server resources.
14+
1115
## Annotation [/annotation{?path}]
1216

1317
### Get annotation for a file [GET]
@@ -37,6 +41,8 @@ unless authentication bearer tokens are configured in the web application and us
3741

3842
### reloads authorization framework [POST]
3943

44+
This is asynchronous API endpoint.
45+
4046
+ Request (application/text)
4147

4248
+ Response 204
@@ -61,6 +67,8 @@ unless authentication bearer tokens are configured in the web application and us
6167

6268
### sets configuration from XML representation [PUT]
6369

70+
This is asynchronous API endpoint.
71+
6472
+ Request (application/xml)
6573
+ Body
6674

@@ -85,6 +93,8 @@ unless authentication bearer tokens are configured in the web application and us
8593

8694
### sets specific configuration field [PUT]
8795

96+
This is asynchronous API endpoint.
97+
8898
+ Parameters
8999
+ reindex (boolean) - specifies if the underlying data were also reindexed (refreshes some searchers and additional data structures)
90100

@@ -577,3 +587,22 @@ This kicks off suggester data rebuild in the background, i.e. the rebuild will v
577587
+ project - project name
578588

579589
+ Response 204
590+
591+
## Status [/status/{uuid}]
592+
593+
### Check the state of API request [GET]
594+
595+
+ Parameters
596+
+ uuid
597+
598+
+ Response 202
599+
600+
### Delete state associated with API request tracking [DELETE]
601+
602+
This should be done only after the API request is completed, i.e. after the GET request for the API request state
603+
returns appropriate status code (e.g. 201).
604+
605+
+ Parameters
606+
+ uuid
607+
608+
+ Response 200

opengrok-indexer/src/main/java/org/opengrok/indexer/configuration/Configuration.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,9 @@ public final class Configuration {
297297

298298
private String serverName; // for reverse proxy environment
299299

300+
private int connectTimeout = -1; // connect timeout in seconds
301+
private int apiTimeout = -1; // API timeout in seconds
302+
300303
/*
301304
* types of handling history for remote SCM repositories:
302305
* ON - index history and display it in webapp
@@ -577,6 +580,8 @@ public Configuration() {
577580
setWebappLAF("default");
578581
// webappCtags is default(boolean)
579582
setXrefTimeout(30);
583+
setApiTimeout(300); // 5 minutes
584+
setConnectTimeout(3);
580585
}
581586

582587
public String getRepoCmd(String clazzName) {
@@ -1385,6 +1390,28 @@ public void setServerName(String serverName) {
13851390
this.serverName = serverName;
13861391
}
13871392

1393+
public int getConnectTimeout() {
1394+
return connectTimeout;
1395+
}
1396+
1397+
public void setConnectTimeout(int connectTimeout) {
1398+
if (connectTimeout < 0) {
1399+
throw new IllegalArgumentException(String.format(NEGATIVE_NUMBER_ERROR, "connectTimeout", connectTimeout));
1400+
}
1401+
this.connectTimeout = connectTimeout;
1402+
}
1403+
1404+
public int getApiTimeout() {
1405+
return apiTimeout;
1406+
}
1407+
1408+
public void setApiTimeout(int apiTimeout) {
1409+
if (apiTimeout < 0) {
1410+
throw new IllegalArgumentException(String.format(NEGATIVE_NUMBER_ERROR, "apiTimeout", apiTimeout));
1411+
}
1412+
this.apiTimeout = apiTimeout;
1413+
}
1414+
13881415
/**
13891416
* Write the current configuration to a file.
13901417
*

opengrok-indexer/src/main/java/org/opengrok/indexer/configuration/RuntimeEnvironment.java

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353

5454
import jakarta.ws.rs.client.ClientBuilder;
5555
import jakarta.ws.rs.client.Entity;
56+
import jakarta.ws.rs.core.HttpHeaders;
5657
import jakarta.ws.rs.core.Response;
5758
import org.apache.lucene.index.IndexReader;
5859
import org.apache.lucene.index.MultiReader;
@@ -61,6 +62,7 @@
6162
import org.apache.lucene.store.Directory;
6263
import org.apache.lucene.store.FSDirectory;
6364
import org.apache.lucene.util.NamedThreadFactory;
65+
import org.jetbrains.annotations.NotNull;
6466
import org.opengrok.indexer.authorization.AuthorizationFramework;
6567
import org.opengrok.indexer.authorization.AuthorizationStack;
6668
import org.opengrok.indexer.history.HistoryGuru;
@@ -1399,6 +1401,22 @@ public void setServerName(String serverName) {
13991401
syncWriteConfiguration(serverName, Configuration::setServerName);
14001402
}
14011403

1404+
public int getApiTimeout() {
1405+
return syncReadConfiguration(Configuration::getApiTimeout);
1406+
}
1407+
1408+
public void setApiTimeout(int apiTimeout) {
1409+
syncWriteConfiguration(apiTimeout, Configuration::setApiTimeout);
1410+
}
1411+
1412+
public int getConnectTimeout() {
1413+
return syncReadConfiguration(Configuration::getConnectTimeout);
1414+
}
1415+
1416+
public void setConnectTimeout(int connectTimeout) {
1417+
syncWriteConfiguration(connectTimeout, Configuration::setConnectTimeout);
1418+
}
1419+
14021420
/**
14031421
* Read an configuration file and set it as the current configuration.
14041422
*
@@ -1440,15 +1458,62 @@ public String getConfigurationXML() {
14401458
}
14411459

14421460
/**
1443-
* Write the current configuration to a socket.
1461+
* Busy waits for API call to complete by repeatedly querying the status API endpoint passed
1462+
* in the {@code Location} header in the response parameter. The overall time is governed
1463+
* by the {@link #getApiTimeout()}, however each individual status check
1464+
* uses {@link #getConnectTimeout()} so in the worst case the total time can be
1465+
* {@code getApiTimeout() * getConnectTimeout()}.
1466+
* @param response response returned from the server upon asynchronous API request
1467+
* @return response from the status API call
1468+
* @throws InterruptedException on sleep interruption
1469+
* @throws IllegalArgumentException on invalid request (no {@code Location} header)
1470+
*/
1471+
private @NotNull Response waitForAsyncApi(@NotNull Response response)
1472+
throws InterruptedException, IllegalArgumentException {
1473+
1474+
String location = response.getHeaderString(HttpHeaders.LOCATION);
1475+
if (location == null) {
1476+
throw new IllegalArgumentException(String.format("no %s header in %s", HttpHeaders.LOCATION, response));
1477+
}
1478+
1479+
LOGGER.log(Level.FINER, "checking asynchronous API result on {0}", location);
1480+
for (int i = 0; i < getApiTimeout(); i++) {
1481+
response = ClientBuilder.newBuilder().
1482+
connectTimeout(RuntimeEnvironment.getInstance().getConnectTimeout(), TimeUnit.SECONDS).build().
1483+
target(location).request().get();
1484+
if (response.getStatus() == Response.Status.ACCEPTED.getStatusCode()) {
1485+
Thread.sleep(1000);
1486+
} else {
1487+
break;
1488+
}
1489+
}
1490+
1491+
if (response.getStatus() == Response.Status.ACCEPTED.getStatusCode()) {
1492+
LOGGER.log(Level.WARNING, "API request still not completed: {0}", response);
1493+
return response;
1494+
}
1495+
1496+
LOGGER.log(Level.FINER, "making DELETE API request to {0}", location);
1497+
Response deleteResponse = ClientBuilder.newBuilder().connectTimeout(3, TimeUnit.SECONDS).build().
1498+
target(location).request().delete();
1499+
if (deleteResponse.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) {
1500+
LOGGER.log(Level.WARNING, "DELETE API call to {0} failed with HTTP error {1}",
1501+
new Object[]{location, response.getStatusInfo()});
1502+
}
1503+
1504+
return response;
1505+
}
1506+
1507+
/**
1508+
* Write the current configuration to a socket and waits for the result.
14441509
*
14451510
* @param host the host address to receive the configuration
14461511
* @throws IOException if an error occurs
14471512
*/
1448-
public void writeConfiguration(String host) throws IOException {
1513+
public void writeConfiguration(String host) throws IOException, InterruptedException, IllegalArgumentException {
14491514
String configXML = syncReadConfiguration(Configuration::getXMLRepresentationAsString);
14501515

1451-
Response r = ClientBuilder.newClient()
1516+
Response response = ClientBuilder.newClient()
14521517
.target(host)
14531518
.path("api")
14541519
.path("v1")
@@ -1458,8 +1523,12 @@ public void writeConfiguration(String host) throws IOException {
14581523
.headers(getWebAppHeaders())
14591524
.put(Entity.xml(configXML));
14601525

1461-
if (r.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) {
1462-
throw new IOException(r.toString());
1526+
if (response.getStatus() == Response.Status.ACCEPTED.getStatusCode()) {
1527+
response = waitForAsyncApi(response);
1528+
}
1529+
1530+
if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) {
1531+
throw new IOException(response.toString());
14631532
}
14641533
}
14651534

opengrok-indexer/src/main/java/org/opengrok/indexer/index/IndexDatabase.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import java.util.concurrent.CopyOnWriteArrayList;
4949
import java.util.concurrent.CountDownLatch;
5050
import java.util.concurrent.ExecutionException;
51+
import java.util.concurrent.TimeUnit;
5152
import java.util.concurrent.atomic.AtomicInteger;
5253
import java.util.logging.Level;
5354
import java.util.logging.Logger;
@@ -378,7 +379,7 @@ private void markProjectIndexed(Project project) {
378379

379380
Response r;
380381
try {
381-
r = ClientBuilder.newClient()
382+
r = ClientBuilder.newBuilder().connectTimeout(env.getConnectTimeout(), TimeUnit.SECONDS).build()
382383
.target(env.getConfigURI())
383384
.path("api")
384385
.path("v1")

opengrok-indexer/src/main/java/org/opengrok/indexer/index/Indexer.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,12 @@ public static String[] parseOptions(String[] argv) throws ParseException {
468468
}
469469
});
470470

471+
parser.on("--apiTimeout", "=number", Integer.class,
472+
"Set timeout for asynchronous API requests.").execute(v -> cfg.setApiTimeout((Integer) v));
473+
474+
parser.on("--connectTimeout", "=number", Integer.class,
475+
"Set connect timeout. Used for API requests.").execute(v -> cfg.setConnectTimeout((Integer) v));
476+
471477
parser.on(
472478
"-A (.ext|prefix.):(-|analyzer)", "--analyzer",
473479
"/(\\.\\w+|\\w+\\.):(-|[a-zA-Z_0-9.]+)/",
@@ -1154,10 +1160,12 @@ public void sendToConfigHost(RuntimeEnvironment env, String host) {
11541160
LOGGER.log(Level.INFO, "Sending configuration to: {0}", host);
11551161
try {
11561162
env.writeConfiguration(host);
1157-
} catch (IOException ex) {
1163+
} catch (IOException | IllegalArgumentException ex) {
11581164
LOGGER.log(Level.SEVERE, String.format(
11591165
"Failed to send configuration to %s "
11601166
+ "(is web application server running with opengrok deployed?)", host), ex);
1167+
} catch (InterruptedException e) {
1168+
LOGGER.log(Level.WARNING, "interrupted while sending configuration");
11611169
}
11621170
LOGGER.info("Configuration update routine done, check log output for errors.");
11631171
}

opengrok-web/src/main/java/org/opengrok/web/WebappListener.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040
import org.opengrok.indexer.index.IndexDatabase;
4141
import org.opengrok.indexer.logger.LoggerFactory;
4242
import org.opengrok.indexer.web.SearchHelper;
43+
import org.opengrok.web.api.ApiTaskManager;
44+
import org.opengrok.web.api.v1.controller.ConfigurationController;
45+
import org.opengrok.web.api.v1.controller.ProjectsController;
4346
import org.opengrok.web.api.v1.suggester.provider.service.SuggesterServiceFactory;
4447

4548
import java.io.File;
@@ -108,6 +111,14 @@ public void contextInitialized(final ServletContextEvent servletContextEvent) {
108111
checkIndex(env);
109112

110113
env.startExpirationTimer();
114+
115+
ApiTaskManager.getInstance().setContextPath(context.getContextPath());
116+
// register API task queues
117+
ApiTaskManager.getInstance().addPool(ProjectsController.PROJECTS_PATH, 1);
118+
// Used by ConfigurationController#reloadAuthorization()
119+
ApiTaskManager.getInstance().addPool("authorization", 1);
120+
ApiTaskManager.getInstance().addPool(ConfigurationController.PATH, 1);
121+
111122
startupTimer.record(Duration.between(start, Instant.now()));
112123
}
113124

@@ -163,6 +174,13 @@ public void contextDestroyed(final ServletContextEvent servletContextEvent) {
163174
// need to explicitly close the suggester service because it might have scheduled rebuild which could prevent
164175
// the web application from closing
165176
SuggesterServiceFactory.getDefault().close();
177+
178+
// destroy queue(s) of API tasks
179+
try {
180+
ApiTaskManager.getInstance().shutdown();
181+
} catch (InterruptedException e) {
182+
LOGGER.log(Level.WARNING, "could not shutdown API task manager cleanly", e);
183+
}
166184
}
167185

168186
/**

0 commit comments

Comments
 (0)