diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java index 68bc040de6f1bb48898eb98dde59c06e2753304d..4039651427c294977b67c1a5ba36d224bdc90e52 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java @@ -7,19 +7,16 @@ import java.util.ArrayList; import java.util.EnumMap; import java.util.List; import java.util.Map; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.function.Function; import java.util.logging.Logger; -import java.util.stream.Collectors; import gov.usgs.earthquake.nshmp.calc.CalcConfig; import gov.usgs.earthquake.nshmp.calc.Hazard; -import gov.usgs.earthquake.nshmp.calc.HazardCalcs; import gov.usgs.earthquake.nshmp.calc.Site; import gov.usgs.earthquake.nshmp.calc.Vs30; import gov.usgs.earthquake.nshmp.data.MutableXySequence; import gov.usgs.earthquake.nshmp.data.XySequence; -import gov.usgs.earthquake.nshmp.eq.model.HazardModel; import gov.usgs.earthquake.nshmp.eq.model.SourceType; import gov.usgs.earthquake.nshmp.geo.Location; import gov.usgs.earthquake.nshmp.gmm.Imt; @@ -29,13 +26,11 @@ import gov.usgs.earthquake.nshmp.internal.www.WsUtils; import gov.usgs.earthquake.nshmp.internal.www.meta.Status; import gov.usgs.earthquake.nshmp.www.BaseModel; import gov.usgs.earthquake.nshmp.www.HazardController; -import gov.usgs.earthquake.nshmp.www.ServletUtil; -import gov.usgs.earthquake.nshmp.www.ServletUtil.Timer; -import gov.usgs.earthquake.nshmp.www.WsUtil; -import gov.usgs.earthquake.nshmp.www.WsUtil.Key; -import gov.usgs.earthquake.nshmp.www.WsUtil.ServiceQueryData; -import gov.usgs.earthquake.nshmp.www.WsUtil.ServiceRequestData; import gov.usgs.earthquake.nshmp.www.meta.Metadata; +import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.Key; +import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.ServiceQueryData; +import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.ServiceRequestData; +import gov.usgs.earthquake.nshmp.www.services.ServletUtil.Timer; import gov.usgs.earthquake.nshmp.www.services.SourceServices.SourceModel; import io.micronaut.http.HttpResponse; @@ -70,10 +65,10 @@ public final class HazardService { var usage = new SourceServices.ResponseData(); var response = new Response<>(Status.USAGE, NAME, urlHelper.url, usage, urlHelper); var svcResponse = ServletUtil.GSON.toJson(response); - LOGGER.info(NAME + " - Response:\n" + ServletUtil.GSON.toJson(svcResponse)); + LOGGER.info(NAME + " - Response:\n" + svcResponse); return HttpResponse.ok(svcResponse); } catch (Exception e) { - return WsUtil.handleError(e, NAME, LOGGER, urlHelper); + return ServicesUtil.handleError(e, NAME, LOGGER, urlHelper); } } @@ -97,10 +92,10 @@ public final class HazardService { var data = new RequestData(query, Vs30.fromValue(query.vs30)); var response = process(data, timer, urlHelper); var svcResponse = ServletUtil.GSON.toJson(response); - LOGGER.info(NAME + " - Response:\n" + ServletUtil.GSON.toJson(svcResponse)); + LOGGER.info(NAME + " - Response:\n" + svcResponse); return HttpResponse.ok(svcResponse); } catch (Exception e) { - return WsUtil.handleError(e, NAME, LOGGER, urlHelper); + return ServicesUtil.handleError(e, NAME, LOGGER, urlHelper); } } @@ -108,7 +103,10 @@ public final class HazardService { RequestData data, Timer timer, UrlHelper urlHelper) throws InterruptedException, ExecutionException { - var hazard = calc(data); + var configFunction = new ConfigFunction(); + var siteFunction = new SiteFunction(data); + var hazard = ServicesUtil.calcHazard(configFunction, siteFunction); + return new ResultBuilder() .hazard(hazard) .requestData(data) @@ -117,41 +115,31 @@ public final class HazardService { .build(); } - static Hazard calc(RequestData data) throws InterruptedException, ExecutionException { - var futuresList = ServletUtil.installedModel().models().stream() - .map(baseModel -> calcHazard(baseModel, ServletUtil.hazardModels().get(baseModel), data)) - .collect(Collectors.toList()); - - var hazardsFuture = CompletableFuture - .allOf(futuresList.toArray(new CompletableFuture[futuresList.size()])) - .thenApply(v -> { - return futuresList.stream() - .map(future -> future.join()) - .collect(Collectors.toList()); - }); - - var hazards = hazardsFuture.get().toArray(new Hazard[] {}); - return Hazard.merge(hazards); + static class ConfigFunction implements Function<BaseModel, CalcConfig> { + @Override + public CalcConfig apply(BaseModel baseModel) { + var hazardModel = ServletUtil.hazardModels().get(baseModel); + var configBuilder = CalcConfig.copyOf(hazardModel.config()); + configBuilder.imts(baseModel.imts); + return configBuilder.build(); + } } - static CompletableFuture<Hazard> calcHazard( - BaseModel baseModel, - HazardModel hazardModel, - RequestData data) { - var location = Location.create(data.latitude, data.longitude); - var configBuilder = CalcConfig.copyOf(hazardModel.config()); - configBuilder.imts(baseModel.imts); - var config = configBuilder.build(); - var site = Site.builder() - .basinDataProvider(config.siteData.basinDataProvider) - .location(location) - .vs30(data.vs30.value()) - .build(); + static class SiteFunction implements Function<CalcConfig, Site> { + final RequestData data; - return CompletableFuture - .supplyAsync( - () -> HazardCalcs.hazard(hazardModel, config, site, ServletUtil.CALC_EXECUTOR), - ServletUtil.TASK_EXECUTOR); + private SiteFunction(RequestData data) { + this.data = data; + } + + @Override + public Site apply(CalcConfig config) { + return Site.builder() + .basinDataProvider(config.siteData.basinDataProvider) + .location(Location.create(data.latitude, data.longitude)) + .vs30(data.vs30.value()) + .build(); + } } public static class Query extends ServiceQueryData { @@ -174,7 +162,7 @@ public final class HazardService { } } - static final class RequestData extends ServiceRequestData { + static class RequestData extends ServiceRequestData { final Vs30 vs30; RequestData(Query query, Vs30 vs30) { diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java index b24499194e513af09f819943eddda6ffa80c6d29..d88510f5baf37aa7d3ddd9c55f065365e680ee6c 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java @@ -21,15 +21,13 @@ import gov.usgs.earthquake.nshmp.internal.www.meta.ParamType; import gov.usgs.earthquake.nshmp.internal.www.meta.Status; import gov.usgs.earthquake.nshmp.mfd.Mfds; import gov.usgs.earthquake.nshmp.www.RateController; -import gov.usgs.earthquake.nshmp.www.ServletUtil; -import gov.usgs.earthquake.nshmp.www.ServletUtil.Timer; -import gov.usgs.earthquake.nshmp.www.WsUtil; -import gov.usgs.earthquake.nshmp.www.WsUtil.Key; -import gov.usgs.earthquake.nshmp.www.WsUtil.ServiceQueryData; -import gov.usgs.earthquake.nshmp.www.WsUtil.ServiceRequestData; import gov.usgs.earthquake.nshmp.www.meta.DoubleParameter; import gov.usgs.earthquake.nshmp.www.meta.Metadata; import gov.usgs.earthquake.nshmp.www.meta.Metadata.DefaultParameters; +import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.Key; +import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.ServiceQueryData; +import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.ServiceRequestData; +import gov.usgs.earthquake.nshmp.www.services.ServletUtil.Timer; import io.micronaut.http.HttpResponse; @@ -66,7 +64,7 @@ public final class RateService { var svcResponse = ServletUtil.GSON.toJson(response); return HttpResponse.ok(String.format(svcResponse, urlHelper.urlPrefix, urlHelper.urlPrefix)); } catch (Exception e) { - return WsUtil.handleError(e, service.name, LOGGER, urlHelper); + return ServicesUtil.handleError(e, service.name, LOGGER, urlHelper); } } @@ -98,7 +96,7 @@ public final class RateService { LOGGER.info(service.name + " - Response:\n" + svcResponse); return HttpResponse.ok(svcResponse); } catch (Exception e) { - return WsUtil.handleError(e, service.name, LOGGER, urlHelper); + return ServicesUtil.handleError(e, service.name, LOGGER, urlHelper); } } diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServicesUtil.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServicesUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..f3ef2cb6e46c2893e78789beecd021866731255d --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServicesUtil.java @@ -0,0 +1,139 @@ +package gov.usgs.earthquake.nshmp.www.services; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.function.Function; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import com.google.gson.GsonBuilder; + +import gov.usgs.earthquake.nshmp.calc.CalcConfig; +import gov.usgs.earthquake.nshmp.calc.Hazard; +import gov.usgs.earthquake.nshmp.calc.HazardCalcs; +import gov.usgs.earthquake.nshmp.calc.Site; +import gov.usgs.earthquake.nshmp.internal.www.NshmpMicronautServlet.UrlHelper; +import gov.usgs.earthquake.nshmp.internal.www.Response; +import gov.usgs.earthquake.nshmp.internal.www.WsUtils; +import gov.usgs.earthquake.nshmp.internal.www.meta.Status; +import gov.usgs.earthquake.nshmp.www.BaseModel; +import gov.usgs.earthquake.nshmp.www.services.SourceServices.SourceModel; + +import io.micronaut.http.HttpResponse; + +class ServicesUtil { + + static HttpResponse<String> handleError( + Throwable e, + String name, + Logger logger, + UrlHelper urlHelper) { + var msg = e.getMessage() + " (see logs)"; + var svcResponse = new Response<>(Status.ERROR, name, urlHelper.url, msg, urlHelper); + var gson = new GsonBuilder().setPrettyPrinting().create(); + var response = gson.toJson(svcResponse); + logger.severe(name + " -\n" + response); + e.printStackTrace(); + return HttpResponse.serverError(response); + } + + static Hazard calcHazard( + Function<BaseModel, CalcConfig> configFunction, + Function<CalcConfig, Site> siteFunction) throws InterruptedException, ExecutionException { + var futuresList = ServletUtil.installedModel().models().stream() + .map(baseModel -> { + var config = configFunction.apply(baseModel); + var site = siteFunction.apply(config); + return calcHazard(baseModel, config, site); + }) + .collect(Collectors.toList()); + + var hazardsFuture = CompletableFuture + .allOf(futuresList.toArray(new CompletableFuture[futuresList.size()])) + .thenApply(v -> { + return futuresList.stream() + .map(future -> future.join()) + .collect(Collectors.toList()); + }); + + var hazards = hazardsFuture.get().toArray(new Hazard[] {}); + return Hazard.merge(hazards); + } + + static class ServiceQueryData implements ServiceQuery { + public final Double longitude; + public final Double latitude; + + ServiceQueryData(Double longitude, Double latitude) { + this.longitude = longitude; + this.latitude = latitude; + } + + @Override + public boolean isNull() { + return longitude == null && latitude == null; + } + + @Override + public void checkValues() { + WsUtils.checkValue(Key.LONGITUDE, longitude); + WsUtils.checkValue(Key.LATITUDE, latitude); + } + } + + static class ServiceRequestData { + public final SourceModel model; + public final double longitude; + public final double latitude; + + public ServiceRequestData(ServiceQueryData query) { + model = new SourceModel(ServletUtil.installedModel()); + longitude = query.longitude; + latitude = query.latitude; + } + } + + enum Key { + EDITION, + REGION, + MODEL, + VS30, + LATITUDE, + LONGITUDE, + IMT, + RETURNPERIOD, + DISTANCE, + FORMAT, + TIMESPAN, + BASIN; + + private String label; + + private Key() { + label = name().toLowerCase(); + } + + @Override + public String toString() { + return label; + } + } + + private static interface ServiceQuery { + boolean isNull(); + + void checkValues(); + } + + private static CompletableFuture<Hazard> calcHazard( + BaseModel baseModel, + CalcConfig config, + Site site) { + return CompletableFuture + .supplyAsync( + () -> HazardCalcs.hazard( + ServletUtil.hazardModels().get(baseModel), config, site, ServletUtil.CALC_EXECUTOR), + ServletUtil.TASK_EXECUTOR); + } + +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServletUtil.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServletUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..e11d2e22e19cf7f4c0e74453c0d8be5c1d287fdf --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServletUtil.java @@ -0,0 +1,183 @@ +package gov.usgs.earthquake.nshmp.www.services; + +import static java.lang.Runtime.getRuntime; + +import java.net.URI; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.format.DateTimeFormatter; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import com.google.common.base.Stopwatch; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import gov.usgs.earthquake.nshmp.calc.Site; +import gov.usgs.earthquake.nshmp.calc.ValueFormat; +import gov.usgs.earthquake.nshmp.calc.Vs30; +import gov.usgs.earthquake.nshmp.eq.model.HazardModel; +import gov.usgs.earthquake.nshmp.gmm.Imt; +import gov.usgs.earthquake.nshmp.internal.www.meta.ParamType; +import gov.usgs.earthquake.nshmp.www.BaseModel; +import gov.usgs.earthquake.nshmp.www.Model; +import gov.usgs.earthquake.nshmp.www.meta.MetaUtil; +import gov.usgs.earthquake.nshmp.www.meta.Region; + +import io.micronaut.context.annotation.Value; +import io.micronaut.context.event.ShutdownEvent; +import io.micronaut.context.event.StartupEvent; +import io.micronaut.runtime.event.annotation.EventListener; + +/** + * Micronaut controller utility objects and methods. + * + * @author U.S. Geological Survey + */ +public class ServletUtil { + + public static final Gson GSON; + public static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern( + "yyyy-MM-dd'T'HH:mm:ssXXX"); + + static final ListeningExecutorService CALC_EXECUTOR; + static final ExecutorService TASK_EXECUTOR; + + static final int THREAD_COUNT; + + /* Stateful flag to reject requests while a result is pending. */ + static boolean uhtBusy = false; + static long hitCount = 0; + static long missCount = 0; + + @Value("${nshmp-haz.installed-model}") + private Model model; + + private static Model INSTALLED_MODEL; + private static Map<BaseModel, HazardModel> HAZARD_MODELS = new EnumMap<>(BaseModel.class); + + static { + /* TODO modified for deagg-epsilon branch; should be context var */ + THREAD_COUNT = getRuntime().availableProcessors(); + CALC_EXECUTOR = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(THREAD_COUNT)); + TASK_EXECUTOR = Executors.newSingleThreadExecutor(); + GSON = new GsonBuilder() + .registerTypeAdapter(Region.class, new MetaUtil.EnumSerializer<Region>()) + .registerTypeAdapter(Imt.class, new MetaUtil.EnumSerializer<Imt>()) + .registerTypeAdapter(Vs30.class, new MetaUtil.EnumSerializer<Vs30>()) + .registerTypeAdapter(ValueFormat.class, new MetaUtil.EnumSerializer<ValueFormat>()) + .registerTypeAdapter(Double.class, new MetaUtil.DoubleSerializer()) + .registerTypeAdapter(ParamType.class, new MetaUtil.ParamTypeSerializer()) + .registerTypeAdapter(Site.class, new MetaUtil.SiteSerializer()) + .disableHtmlEscaping() + .serializeNulls() + .setPrettyPrinting() + .create(); + } + + static Model installedModel() { + return INSTALLED_MODEL; + } + + static Map<BaseModel, HazardModel> hazardModels() { + return HAZARD_MODELS; + } + + @EventListener + void shutdown(ShutdownEvent event) { + CALC_EXECUTOR.shutdown(); + TASK_EXECUTOR.shutdown(); + } + + @EventListener + void startup(StartupEvent event) { + INSTALLED_MODEL = model; + + model.models().forEach(baseModel -> { + HAZARD_MODELS.put(baseModel, loadModel(baseModel)); + }); + + HAZARD_MODELS = Map.copyOf(HAZARD_MODELS); + } + + private HazardModel loadModel(BaseModel model) { + Path path; + URL url; + URI uri; + String uriString; + String[] uriParts; + FileSystem fs; + + try { + url = Paths.get(model.path).toUri().toURL(); + uri = new URI(url.toString().replace(" ", "%20")); + uriString = uri.toString(); + + /* + * When the web sevice is deployed inside a JAR file (and not unpacked by + * the servlet container) model resources will not exist on disk as + * otherwise expected. In this case, load the resources directly out of + * the JAR file as well. This is slower, but with the preload option + * enabled it may be less of an issue if the models are already in memory. + */ + + if (uriString.indexOf("!") != -1) { + uriParts = uri.toString().split("!"); + + try { + fs = FileSystems.getFileSystem( + URI.create(uriParts[0])); + } catch (FileSystemNotFoundException fnx) { + fs = FileSystems.newFileSystem( + URI.create(uriParts[0]), + new HashMap<String, String>()); + } + + path = fs.getPath(uriParts[1].replaceAll("%20", " ")); + } else { + path = Paths.get(uri); + } + + return HazardModel.load(path); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static Timer timer() { + return new Timer(); + } + + /* + * Simple timer object. The servlet timer just runs. The calculation timer can + * be started later. + */ + public static final class Timer { + Stopwatch servlet = Stopwatch.createStarted(); + Stopwatch calc = Stopwatch.createUnstarted(); + + public Timer start() { + calc.start(); + return this; + } + + public String servletTime() { + return servlet.toString(); + } + + public String calcTime() { + return calc.toString(); + } + } + +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java index 8201509398c34895250165517a3c19b26f14aa3f..99c80fb53d9eb1befd88f6ed6f7bd379ededb303 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java @@ -23,8 +23,6 @@ import gov.usgs.earthquake.nshmp.internal.www.meta.ParamType; import gov.usgs.earthquake.nshmp.internal.www.meta.Status; import gov.usgs.earthquake.nshmp.www.BaseModel; import gov.usgs.earthquake.nshmp.www.Model; -import gov.usgs.earthquake.nshmp.www.ServletUtil; -import gov.usgs.earthquake.nshmp.www.WsUtil; import gov.usgs.earthquake.nshmp.www.meta.DoubleParameter; import gov.usgs.earthquake.nshmp.www.meta.MetaUtil; import gov.usgs.earthquake.nshmp.www.meta.Metadata; @@ -71,7 +69,7 @@ public class SourceServices { LOGGER.info(NAME + "- Response:\n" + jsonString); return HttpResponse.ok(jsonString); } catch (Exception e) { - return WsUtil.handleError(e, NAME, LOGGER, urlHelper); + return ServicesUtil.handleError(e, NAME, LOGGER, urlHelper); } }