Skip to content

Commit 81d222e

Browse files
authored
Add GetServiceState action for MoSAPI service monitoring (#2906)
* Add GetServiceState action for MoSAPI service monitoring Implements the `/api/mosapi/getServiceState` endpoint to retrieve service health summaries for TLDs from the MoSAPI system. - Introduces `GetServiceStateAction` to fetch TLD service status. - Implements `MosApiStateService` to transform raw MoSAPI responses into a curated `ServiceStateSummary`. - Uses concurrent processing with a fixed thread pool to fetch states for all configured TLDs efficiently while respecting MoSAPI rate limits. junit test added * Refactor MoSAPI models to records and address review nits - Convert model classes to Java records for conciseness and immutability. - Update unit tests to use Java text blocks for improved JSON readability. - Simplify service and action layers by removing redundant logic and logging. - Fix configuration nits regarding primitive types and comment formatting. * Consolidate MoSAPI models and enhance null-safety - Moves model records into a single MosApiModels.java file. - Switches to ImmutableList/ImmutableMap with non-null defaults in constructors. - Removes redundant pass-through methods in MosApiStateService. - Updates tests to use Java Text Blocks and non-null collection assertions. * Improve MoSAPI client error handling and clean up data models Refactors the MoSAPI monitoring client to be more robust against infrastructure failures * Refactor: use nullToEmptyImmutableCopy() for MoSAPI models Standardize null-handling in model classes by using the Nomulus `nullToEmptyImmutableCopy()` utility. This ensures consistent API responses with empty lists instead of omitted fields.
1 parent 7e9d4c2 commit 81d222e

21 files changed

+1059
-5
lines changed

core/src/main/java/google/registry/config/RegistryConfig.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1462,6 +1462,12 @@ public static ImmutableSet<String> provideMosapiServices(RegistryConfigSettings
14621462
return ImmutableSet.copyOf(config.mosapi.services);
14631463
}
14641464

1465+
@Provides
1466+
@Config("mosapiTldThreadCnt")
1467+
public static int provideMosapiTldThreads(RegistryConfigSettings config) {
1468+
return config.mosapi.tldThreadCnt;
1469+
}
1470+
14651471
private static String formatComments(String text) {
14661472
return Splitter.on('\n').omitEmptyStrings().trimResults().splitToList(text).stream()
14671473
.map(s -> "# " + s)

core/src/main/java/google/registry/config/RegistryConfigSettings.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,5 +272,6 @@ public static class MosApi {
272272
public String entityType;
273273
public List<String> tlds;
274274
public List<String> services;
275+
public int tldThreadCnt;
275276
}
276277
}

core/src/main/java/google/registry/config/files/default-config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,4 +642,8 @@ mosapi:
642642
- "epp"
643643
- "dnssec"
644644

645+
# Provides a fixed thread pool for parallel TLD processing.
646+
# @see <a href="https://www.icann.org/mosapi-specification.pdf">
647+
# ICANN MoSAPI Specification, Section 12.3</a>
648+
tldThreadCnt: 4
645649

core/src/main/java/google/registry/module/RequestComponent.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@
6262
import google.registry.module.ReadinessProbeAction.ReadinessProbeActionPubApi;
6363
import google.registry.module.ReadinessProbeAction.ReadinessProbeConsoleAction;
6464
import google.registry.monitoring.whitebox.WhiteboxModule;
65+
import google.registry.mosapi.GetServiceStateAction;
66+
import google.registry.mosapi.module.MosApiRequestModule;
6567
import google.registry.rdap.RdapAutnumAction;
6668
import google.registry.rdap.RdapDomainAction;
6769
import google.registry.rdap.RdapDomainSearchAction;
@@ -151,6 +153,7 @@
151153
EppToolModule.class,
152154
IcannReportingModule.class,
153155
LoadTestModule.class,
156+
MosApiRequestModule.class,
154157
RdapModule.class,
155158
RdeModule.class,
156159
ReportingModule.class,
@@ -232,6 +235,8 @@ interface RequestComponent {
232235

233236
GenerateZoneFilesAction generateZoneFilesAction();
234237

238+
GetServiceStateAction getServiceStateAction();
239+
235240
IcannReportingStagingAction icannReportingStagingAction();
236241

237242
IcannReportingUploadAction icannReportingUploadAction();
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package google.registry.mosapi;
16+
17+
import com.google.common.net.MediaType;
18+
import com.google.gson.Gson;
19+
import google.registry.request.Action;
20+
import google.registry.request.HttpException.ServiceUnavailableException;
21+
import google.registry.request.Parameter;
22+
import google.registry.request.Response;
23+
import google.registry.request.auth.Auth;
24+
import jakarta.inject.Inject;
25+
import java.util.Optional;
26+
27+
/** An action that returns the current MoSAPI service state for a given TLD or all TLDs. */
28+
@Action(
29+
service = Action.Service.BACKEND,
30+
path = GetServiceStateAction.PATH,
31+
method = Action.Method.GET,
32+
auth = Auth.AUTH_ADMIN)
33+
public class GetServiceStateAction implements Runnable {
34+
35+
public static final String PATH = "/_dr/mosapi/getServiceState";
36+
public static final String TLD_PARAM = "tld";
37+
38+
private final MosApiStateService stateService;
39+
private final Response response;
40+
private final Gson gson;
41+
private final Optional<String> tld;
42+
43+
@Inject
44+
public GetServiceStateAction(
45+
MosApiStateService stateService,
46+
Response response,
47+
Gson gson,
48+
@Parameter(TLD_PARAM) Optional<String> tld) {
49+
this.stateService = stateService;
50+
this.response = response;
51+
this.gson = gson;
52+
this.tld = tld;
53+
}
54+
55+
@Override
56+
public void run() {
57+
response.setContentType(MediaType.JSON_UTF_8);
58+
try {
59+
if (tld.isPresent()) {
60+
response.setPayload(gson.toJson(stateService.getServiceStateSummary(tld.get())));
61+
} else {
62+
response.setPayload(gson.toJson(stateService.getAllServiceStateSummaries()));
63+
}
64+
} catch (MosApiException e) {
65+
throw new ServiceUnavailableException("Error fetching MoSAPI service state.");
66+
}
67+
}
68+
}

core/src/main/java/google/registry/mosapi/model/MosApiErrorResponse.java renamed to core/src/main/java/google/registry/mosapi/MosApiErrorResponse.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
package google.registry.mosapi.model;
15+
package google.registry.mosapi;
16+
17+
import com.google.gson.annotations.Expose;
1618

1719
/**
1820
* Represents the generic JSON error response from the MoSAPI service for a 400 Bad Request.
1921
*
2022
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification, Section
2123
* 8</a>
2224
*/
23-
public record MosApiErrorResponse(String resultCode, String message, String description) {}
25+
public record MosApiErrorResponse(
26+
@Expose String resultCode, @Expose String message, @Expose String description) {}

core/src/main/java/google/registry/mosapi/MosApiException.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import static java.lang.annotation.ElementType.TYPE;
1818
import static java.lang.annotation.RetentionPolicy.RUNTIME;
1919

20-
import google.registry.mosapi.model.MosApiErrorResponse;
2120
import java.io.IOException;
2221
import java.lang.annotation.Documented;
2322
import java.lang.annotation.Retention;
@@ -42,6 +41,11 @@ public MosApiException(String message, Throwable cause) {
4241
this.errorResponse = null;
4342
}
4443

44+
public MosApiException(String message) {
45+
super(message);
46+
this.errorResponse = null;
47+
}
48+
4549
public Optional<MosApiErrorResponse> getErrorResponse() {
4650
return Optional.ofNullable(errorResponse);
4751
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package google.registry.mosapi;
16+
17+
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
18+
19+
import com.google.gson.annotations.Expose;
20+
import com.google.gson.annotations.SerializedName;
21+
import java.util.List;
22+
import java.util.Map;
23+
import javax.annotation.Nullable;
24+
25+
/** Data models for ICANN MoSAPI. */
26+
public final class MosApiModels {
27+
28+
private MosApiModels() {}
29+
30+
/**
31+
* A wrapper response containing the state summaries of all monitored services.
32+
*
33+
* <p>This corresponds to the collection of service statuses returned when monitoring the state of
34+
* a TLD
35+
*
36+
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification,
37+
* Section 5.1</a>
38+
*/
39+
public record AllServicesStateResponse(
40+
// A list of state summaries for each monitored service (e.g. DNS, RDDS, etc.)
41+
@Expose List<ServiceStateSummary> serviceStates) {
42+
43+
public AllServicesStateResponse {
44+
serviceStates = nullToEmptyImmutableCopy(serviceStates);
45+
}
46+
}
47+
48+
/**
49+
* A summary of a service incident.
50+
*
51+
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification,
52+
* Section 5.1</a>
53+
*/
54+
public record IncidentSummary(
55+
@Expose String incidentID,
56+
@Expose long startTime,
57+
@Expose boolean falsePositive,
58+
@Expose String state,
59+
@Expose @Nullable Long endTime) {}
60+
61+
/**
62+
* A curated summary of the service state for a TLD.
63+
*
64+
* <p>This class aggregates the high-level status of a TLD and details of any active incidents
65+
* affecting specific services (like DNS or RDDS), based on the data structures defined in the
66+
* MoSAPI specification.
67+
*
68+
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification,
69+
* Section 5.1</a>
70+
*/
71+
public record ServiceStateSummary(
72+
@Expose String tld,
73+
@Expose String overallStatus,
74+
@Expose List<ServiceStatus> activeIncidents) {
75+
76+
public ServiceStateSummary {
77+
activeIncidents = nullToEmptyImmutableCopy(activeIncidents);
78+
}
79+
}
80+
81+
/** Represents the status of a single monitored service. */
82+
public record ServiceStatus(
83+
/**
84+
* A JSON string that contains the status of the Service as seen from the monitoring system.
85+
* Possible values include "Up", "Down", "Disabled", "UP-inconclusive-no-data", etc.
86+
*/
87+
@Expose String status,
88+
89+
// A JSON number that contains the current percentage of the Emergency Threshold
90+
// of the Service. A value of "0" specifies that there are no Incidents
91+
// affecting the threshold.
92+
@Expose double emergencyThreshold,
93+
@Expose List<IncidentSummary> incidents) {
94+
95+
public ServiceStatus {
96+
incidents = nullToEmptyImmutableCopy(incidents);
97+
}
98+
}
99+
100+
/**
101+
* Represents the overall health of all monitored services for a TLD.
102+
*
103+
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification,
104+
* Section 5.1</a>
105+
*/
106+
public record TldServiceState(
107+
@Expose String tld,
108+
long lastUpdateApiDatabase,
109+
110+
// A JSON string that contains the status of the TLD as seen from the monitoring system
111+
@Expose String status,
112+
113+
// A JSON object containing detailed information for each potential monitored service (i.e.,
114+
// DNS,
115+
// RDDS, EPP, DNSSEC, RDAP).
116+
@Expose @SerializedName("testedServices") Map<String, ServiceStatus> serviceStatuses) {
117+
118+
public TldServiceState {
119+
serviceStatuses = nullToEmptyImmutableCopy(serviceStatuses);
120+
}
121+
}
122+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package google.registry.mosapi;
16+
17+
import static com.google.common.collect.ImmutableList.toImmutableList;
18+
19+
import com.google.common.collect.ImmutableList;
20+
import com.google.common.collect.ImmutableSet;
21+
import com.google.common.flogger.FluentLogger;
22+
import google.registry.config.RegistryConfig.Config;
23+
import google.registry.mosapi.MosApiModels.AllServicesStateResponse;
24+
import google.registry.mosapi.MosApiModels.ServiceStateSummary;
25+
import google.registry.mosapi.MosApiModels.ServiceStatus;
26+
import google.registry.mosapi.MosApiModels.TldServiceState;
27+
import jakarta.inject.Inject;
28+
import jakarta.inject.Named;
29+
import java.util.concurrent.CompletableFuture;
30+
import java.util.concurrent.ExecutorService;
31+
32+
/** A service that provides business logic for interacting with MoSAPI Service State. */
33+
public class MosApiStateService {
34+
35+
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
36+
private final ServiceMonitoringClient serviceMonitoringClient;
37+
private final ExecutorService tldExecutor;
38+
39+
private final ImmutableSet<String> tlds;
40+
41+
private static final String DOWN_STATUS = "Down";
42+
private static final String FETCH_ERROR_STATUS = "ERROR";
43+
44+
@Inject
45+
public MosApiStateService(
46+
ServiceMonitoringClient serviceMonitoringClient,
47+
@Config("mosapiTlds") ImmutableSet<String> tlds,
48+
@Named("mosapiTldExecutor") ExecutorService tldExecutor) {
49+
this.serviceMonitoringClient = serviceMonitoringClient;
50+
this.tlds = tlds;
51+
this.tldExecutor = tldExecutor;
52+
}
53+
54+
/** Fetches and transforms the service state for a given TLD into a summary. */
55+
public ServiceStateSummary getServiceStateSummary(String tld) throws MosApiException {
56+
TldServiceState rawState = serviceMonitoringClient.getTldServiceState(tld);
57+
return transformToSummary(rawState);
58+
}
59+
60+
/** Fetches and transforms the service state for all configured TLDs. */
61+
public AllServicesStateResponse getAllServiceStateSummaries() {
62+
ImmutableList<CompletableFuture<ServiceStateSummary>> futures =
63+
tlds.stream()
64+
.map(
65+
tld ->
66+
CompletableFuture.supplyAsync(
67+
() -> {
68+
try {
69+
return getServiceStateSummary(tld);
70+
} catch (MosApiException e) {
71+
logger.atWarning().withCause(e).log(
72+
"Failed to get service state for TLD %s.", tld);
73+
// we don't want to throw exception if fetch failed
74+
return new ServiceStateSummary(tld, FETCH_ERROR_STATUS, null);
75+
}
76+
},
77+
tldExecutor))
78+
.collect(ImmutableList.toImmutableList());
79+
80+
ImmutableList<ServiceStateSummary> summaries =
81+
futures.stream()
82+
.map(CompletableFuture::join) // Waits for all tasks to complete
83+
.collect(toImmutableList());
84+
85+
return new AllServicesStateResponse(summaries);
86+
}
87+
88+
private ServiceStateSummary transformToSummary(TldServiceState rawState) {
89+
ImmutableList<ServiceStatus> activeIncidents = ImmutableList.of();
90+
if (DOWN_STATUS.equalsIgnoreCase(rawState.status())) {
91+
activeIncidents =
92+
rawState.serviceStatuses().entrySet().stream()
93+
.filter(
94+
entry -> {
95+
ServiceStatus serviceStatus = entry.getValue();
96+
return serviceStatus.incidents() != null
97+
&& !serviceStatus.incidents().isEmpty();
98+
})
99+
.map(
100+
entry ->
101+
new ServiceStatus(
102+
// key is the service name
103+
entry.getKey(),
104+
entry.getValue().emergencyThreshold(),
105+
entry.getValue().incidents()))
106+
.collect(toImmutableList());
107+
}
108+
return new ServiceStateSummary(rawState.tld(), rawState.status(), activeIncidents);
109+
}
110+
}

0 commit comments

Comments
 (0)