From af6752a5763ff56eacf3b77d2e4c8ca0b1828fe0 Mon Sep 17 00:00:00 2001 From: Olivier Maury <Olivier.Maury@inrae.fr> Date: Tue, 7 May 2024 17:28:44 +0200 Subject: [PATCH 1/2] =?UTF-8?q?Rafra=C3=AEchir=20la=20vue=20mat=C3=A9riali?= =?UTF-8?q?s=C3=A9e=20et=20vider=20le=20cache.=20fixes=20#10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- sql/schema.functions.sql | 1 + .../seasonhandler/MainConfiguration.java | 39 +++++++- .../seasonhandler/cmd/MainCommand.java | 11 +++ .../seasonhandler/di/DiHelper.java | 2 +- .../jms/IntegrationDoneProducer.java | 43 +++++++++ .../jms/IntegrationDoneReceiver.java | 92 +++++++++++++++++++ .../jms/SimulationDoneReceiver.java | 7 ++ .../config-evaluation-not-exists.properties | 2 + .../config-good-with-stages.properties | 2 + src/test/resources/config-good.properties | 2 + .../config-invalid-stages.properties | 2 + .../config-simulation-not-exists.properties | 2 + 13 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 src/main/java/fr/agrometinfo/seasonhandler/jms/IntegrationDoneProducer.java create mode 100644 src/main/java/fr/agrometinfo/seasonhandler/jms/IntegrationDoneReceiver.java diff --git a/pom.xml b/pom.xml index 80eddfc..17c2ff7 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ <modelVersion>4.0.0</modelVersion> <groupId>fr.agrometinfo</groupId> <artifactId>season-handler</artifactId> - <version>2.0.0-alpha-3</version> + <version>2.0.0-SNAPSHOT</version> <packaging>jar</packaging> <name>AgroMetInfo SEASON handler</name> <description>SEASON handler for AgroMetInfo</description> diff --git a/sql/schema.functions.sql b/sql/schema.functions.sql index 36b8b17..a49bbea 100644 --- a/sql/schema.functions.sql +++ b/sql/schema.functions.sql @@ -25,6 +25,7 @@ ON CONFLICT ON CONSTRAINT "UK_dailyvalue" DO UPDATE SET computedvalue=EXCLUDED.computedvalue, comparedvalue=EXCLUDED.comparedvalue, created=CURRENT_TIMESTAMP; $$, tablename); EXECUTE sql; + REFRESH MATERIALIZED VIEW v_pra_dailyvalue; END; $BODY$ LANGUAGE plpgsql VOLATILE diff --git a/src/main/java/fr/agrometinfo/seasonhandler/MainConfiguration.java b/src/main/java/fr/agrometinfo/seasonhandler/MainConfiguration.java index d7e9ce1..b817343 100644 --- a/src/main/java/fr/agrometinfo/seasonhandler/MainConfiguration.java +++ b/src/main/java/fr/agrometinfo/seasonhandler/MainConfiguration.java @@ -68,6 +68,10 @@ public class MainConfiguration { * Properties keys for config.properties. */ public enum Property { + /** + * REST URL to empty cache in the AgroMetInfo web app. + */ + EMPTY_CACHE_REST_URL("rest.empty.cache.rest.url", true), /** * File path for evaluation to run. */ @@ -138,6 +142,12 @@ public class MainConfiguration { */ private final I18n i18n = DiHelper.getI18N(); + /** + * REST URLs to empty cache in the AgroMetInfo web app. + */ + @Getter + private final List<String> emptyCacheRestUrls = new ArrayList<>(); + /** * Properties from configuration file. */ @@ -241,6 +251,18 @@ public class MainConfiguration { return map; } + /** + * @param property a property + * @return all suffixes of configured keys + */ + private List<String> getKeySuffixes(final Property property) { + return properties.stringPropertyNames().stream() // + .filter(p -> p.startsWith(property.getKey())) // + .map(p -> p.replace(property.getKey() + ".", "")) // + .sorted() // + .toList(); + } + /** * Make some checks on the file and load {@link SimulationProperties}. * @@ -315,9 +337,20 @@ public class MainConfiguration { return Optional.of(i18n.format("error.config.missingkeys", String.join(",", missingKeys))); } initPersistenceUnit(); + initEmptyCacheRestUrls(); return initEvaluations(); } + /** + * Get all URL to empty cache in AgroMetInfo web app. + */ + private void initEmptyCacheRestUrls() { + getKeySuffixes(Property.EMPTY_CACHE_REST_URL).stream() // + .map(suffix -> get(Property.SIMULATION_PATH, suffix)) // + .filter(v -> v != null && !v.isBlank()) // + .forEach(emptyCacheRestUrls::add); + } + /** * Make some checks on the file and load {@link Evaluation}. * @@ -368,11 +401,7 @@ public class MainConfiguration { LOGGER.traceEntry(); evaluations.clear(); stages.clear(); - final List<String> suffixes = properties.stringPropertyNames().stream() // - .filter(p -> p.startsWith(Property.SIMULATION_PATH.getKey())) // - .map(p -> p.replace(Property.SIMULATION_PATH.getKey() + ".", "")) // - .sorted() // - .toList(); + final List<String> suffixes = getKeySuffixes(Property.SIMULATION_PATH); for (final String suffix : suffixes) { LOGGER.trace("Suffix : {}", suffix); Optional<String> res = initEvaluation(suffix); diff --git a/src/main/java/fr/agrometinfo/seasonhandler/cmd/MainCommand.java b/src/main/java/fr/agrometinfo/seasonhandler/cmd/MainCommand.java index 4d47a50..a1da118 100644 --- a/src/main/java/fr/agrometinfo/seasonhandler/cmd/MainCommand.java +++ b/src/main/java/fr/agrometinfo/seasonhandler/cmd/MainCommand.java @@ -36,6 +36,8 @@ import org.apache.logging.log4j.core.config.Configurator; import fr.agrometinfo.seasonhandler.MainConfiguration; import fr.agrometinfo.seasonhandler.di.DiHelper; +import fr.agrometinfo.seasonhandler.jms.IntegrationDoneProducer; +import fr.agrometinfo.seasonhandler.jms.IntegrationDoneReceiver; import fr.agrometinfo.seasonhandler.jms.SafranReceiver; import fr.agrometinfo.seasonhandler.jms.SimulationDoneReceiver; import fr.inrae.agroclim.indicators.resources.I18n; @@ -127,12 +129,21 @@ public final class MainCommand implements Callable<Integer> { diHelper.inject(safranReceiver); safranReceiver.run(); + final Destination integrationDoneQueue = (Destination) initialContext.lookup("integrationDoneQueueLookup"); + final IntegrationDoneProducer integrationDoneSender = new IntegrationDoneProducer(jmsContext, + integrationDoneQueue); + final Destination simulationDoneQueue = (Destination) initialContext.lookup("simulationDoneQueueLookup"); final SimulationDoneReceiver simulationDoneReceiver = new SimulationDoneReceiver(jmsContext, simulationDoneQueue); + simulationDoneReceiver.setIntegrationDoneProducer(integrationDoneSender); diHelper.inject(simulationDoneReceiver); simulationDoneReceiver.run(); + final IntegrationDoneReceiver integrationDoneReceiver = new IntegrationDoneReceiver(jmsContext, + integrationDoneQueue); + integrationDoneReceiver.run(); + jmsContext.start(); // Sleep forever, waiting for ourselves to finish Thread.currentThread().join(); diff --git a/src/main/java/fr/agrometinfo/seasonhandler/di/DiHelper.java b/src/main/java/fr/agrometinfo/seasonhandler/di/DiHelper.java index c831897..8ddd7b0 100644 --- a/src/main/java/fr/agrometinfo/seasonhandler/di/DiHelper.java +++ b/src/main/java/fr/agrometinfo/seasonhandler/di/DiHelper.java @@ -163,7 +163,7 @@ public final class DiHelper { * @param pm JPA persistence manager * @return DAO for {@link SimulationSoilDao}. */ - private SimulationSoilDao proviceSimulationSoilDao(PersistenceManager pm) { + private SimulationSoilDao proviceSimulationSoilDao(final PersistenceManager pm) { return new SimulationSoilDaoHibernate(pm, config.getDatabaseRoles()); } diff --git a/src/main/java/fr/agrometinfo/seasonhandler/jms/IntegrationDoneProducer.java b/src/main/java/fr/agrometinfo/seasonhandler/jms/IntegrationDoneProducer.java new file mode 100644 index 0000000..c0a4373 --- /dev/null +++ b/src/main/java/fr/agrometinfo/seasonhandler/jms/IntegrationDoneProducer.java @@ -0,0 +1,43 @@ +package fr.agrometinfo.seasonhandler.jms; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import javax.jms.Destination; +import javax.jms.JMSContext; +import javax.jms.JMSProducer; +import lombok.extern.log4j.Log4j2; + +/** + * JMS message sender to notify when data integration into AgroMetInfo database is done. + * + * @author Olivier Maury + */ +@Log4j2 +public final class IntegrationDoneProducer { + + /** + * The queue used by the SEASON-handler to notify itself. + */ + private final Destination destination; + + /** + * JMS producer. + */ + private final JMSProducer producer; + + /** + * Constructor. + * + * @param context JMS Context + * @param queue queue to listen + */ + public IntegrationDoneProducer(final JMSContext context, final Destination queue) { + producer = context.createProducer(); + destination = queue; + } + + public void send() { + var body = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + producer.send(destination, body); + } +} diff --git a/src/main/java/fr/agrometinfo/seasonhandler/jms/IntegrationDoneReceiver.java b/src/main/java/fr/agrometinfo/seasonhandler/jms/IntegrationDoneReceiver.java new file mode 100644 index 0000000..6dfacda --- /dev/null +++ b/src/main/java/fr/agrometinfo/seasonhandler/jms/IntegrationDoneReceiver.java @@ -0,0 +1,92 @@ +package fr.agrometinfo.seasonhandler.jms; + +import fr.agrometinfo.seasonhandler.MainConfiguration; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.LocalDateTime; +import javax.jms.Destination; +import javax.jms.JMSConsumer; +import javax.jms.JMSContext; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageListener; +import javax.ws.rs.core.Response; +import lombok.Setter; +import lombok.extern.log4j.Log4j2; +import org.apache.activemq.artemis.jms.client.ActiveMQMessage; + +/** + * JMS message receiver to empty caches in the web apps. + * + * @author Olivier Maury + */ +@Log4j2 +public final class IntegrationDoneReceiver implements MessageListener, Runnable { + + /** + * Consumer of JMS messages. + */ + private final JMSConsumer consumer; + /** + * The last time the REST end point was called. + */ + private LocalDateTime lastDateTime; + /** + * Application configuration. + */ + @Setter + private MainConfiguration config; + + /** + * Constructor. + * + * @param context JMS Context + * @param destination queue to listen + */ + public IntegrationDoneReceiver(final JMSContext context, final Destination destination) { + LOGGER.traceEntry(); + consumer = context.createConsumer(destination); + } + + @Override + public void onMessage(final Message message) { + LOGGER.traceEntry("Message received: {}", message); + if (message instanceof final ActiveMQMessage msg) { + try { + final String body = msg.getBody(String.class); + LOGGER.info("Body from received message: {}", body); + var parsed = LocalDateTime.parse(body); + if (parsed != null && parsed.isAfter(lastDateTime)) { + // empty cache + HttpClient client = HttpClient.newHttpClient(); + config.getEmptyCacheRestUrls().forEach(url -> { + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).build(); + try { + HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != Response.Status.OK.getStatusCode()) { + LOGGER.error("AgroMetInfo did not response OK at {}: {}", url, response); + } + } catch (IOException | InterruptedException ex) { + LOGGER.error("Something went wrong when calling AgroMetInfo REST end point {}!", url, ex); + } + }); + lastDateTime = LocalDateTime.now(); + } + msg.acknowledge(); + } catch (final JMSException ex) { + LOGGER.fatal(ex); + } + } else { + LOGGER.error("Ignoring this message as it is not ActiveMQMessage: {}", message); + } + } + + @Override + public void run() { + consumer.setMessageListener(this); + } + +} diff --git a/src/main/java/fr/agrometinfo/seasonhandler/jms/SimulationDoneReceiver.java b/src/main/java/fr/agrometinfo/seasonhandler/jms/SimulationDoneReceiver.java index e37d1ae..71dd4af 100644 --- a/src/main/java/fr/agrometinfo/seasonhandler/jms/SimulationDoneReceiver.java +++ b/src/main/java/fr/agrometinfo/seasonhandler/jms/SimulationDoneReceiver.java @@ -72,6 +72,12 @@ public final class SimulationDoneReceiver implements MessageListener, Runnable { @Setter private SimulationSoilDao simulationSoilDao; + /** + * JMS producer when integration is done. + */ + @Setter + private IntegrationDoneProducer integrationDoneProducer; + /** * Constructor. * @@ -109,6 +115,7 @@ public final class SimulationDoneReceiver implements MessageListener, Runnable { msg.acknowledge(); amiSimulationDao.setEnded(simulationId); LOGGER.info("Simulation results in {}.{} handled", schemaName, tableName); + integrationDoneProducer.send(); } catch (final JMSException ex) { LOGGER.fatal(ex); } diff --git a/src/test/resources/config-evaluation-not-exists.properties b/src/test/resources/config-evaluation-not-exists.properties index 5825bab..fc126bf 100644 --- a/src/test/resources/config-evaluation-not-exists.properties +++ b/src/test/resources/config-evaluation-not-exists.properties @@ -1,5 +1,7 @@ ## AgroMetInfo-SEASON-handler configuration +rest.empty.cache.rest.url.0 = http://localhost:8080/www-server/rs/application/empty_cache + simulation.path.0 = src/test/resources/simulation-evaluation-does-not-exist.properties ## JMS configuration diff --git a/src/test/resources/config-good-with-stages.properties b/src/test/resources/config-good-with-stages.properties index 2e44f29..a250405 100644 --- a/src/test/resources/config-good-with-stages.properties +++ b/src/test/resources/config-good-with-stages.properties @@ -1,5 +1,7 @@ ## AgroMetInfo-SEASON-handler configuration +rest.empty.cache.rest.url.0 = http://localhost:8080/www-server/rs/application/empty_cache + simulation.path.0 = src/test/resources/simulation-good-with-stages.properties ## JMS configuration diff --git a/src/test/resources/config-good.properties b/src/test/resources/config-good.properties index de0e3c8..d29a073 100644 --- a/src/test/resources/config-good.properties +++ b/src/test/resources/config-good.properties @@ -1,5 +1,7 @@ ## AgroMetInfo-SEASON-handler configuration +rest.empty.cache.rest.url.0 = http://localhost:8080/www-server/rs/application/empty_cache + simulation.path.0 = src/test/resources/simulation-good.properties ## JMS configuration diff --git a/src/test/resources/config-invalid-stages.properties b/src/test/resources/config-invalid-stages.properties index 361b97a..5171ce7 100644 --- a/src/test/resources/config-invalid-stages.properties +++ b/src/test/resources/config-invalid-stages.properties @@ -1,5 +1,7 @@ ## AgroMetInfo-SEASON-handler configuration +rest.empty.cache.rest.url.0 = http://localhost:8080/www-server/rs/application/empty_cache + simulation.path.0 = src/test/resources/simulation-invalid-stages.xml ## JMS configuration diff --git a/src/test/resources/config-simulation-not-exists.properties b/src/test/resources/config-simulation-not-exists.properties index 61d8b05..8378427 100644 --- a/src/test/resources/config-simulation-not-exists.properties +++ b/src/test/resources/config-simulation-not-exists.properties @@ -1,5 +1,7 @@ ## AgroMetInfo-SEASON-handler configuration +rest.empty.cache.rest.url.0 = http://localhost:8080/www-server/rs/application/empty_cache + simulation.path.0 = src/test/resources/simulation-does-not-exist.properties ## JMS configuration -- GitLab From d8e90ec7082e4deeb12046ec22e375b0d2a3221c Mon Sep 17 00:00:00 2001 From: Olivier Maury <Olivier.Maury@inrae.fr> Date: Tue, 7 May 2024 17:38:39 +0200 Subject: [PATCH 2/2] Doc --- src/site/markdown/usage.md | 4 ++++ src/site/resources/images/seq-jms.puml | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/site/markdown/usage.md b/src/site/markdown/usage.md index ca40c95..7bf7276 100644 --- a/src/site/markdown/usage.md +++ b/src/site/markdown/usage.md @@ -37,6 +37,10 @@ A valid example is available in the test sources at `src/test/resources/config-g ```properties # AgroMetInfo-SEASON-handler configuration +## AgroMetInfo REST end point + +rest.empty.cache.rest.url.0 = http://localhost:8080/www-server/rs/application/empty_cache + ## SEASON simulations simulation.path.0 = src/test/resources/simulation-good-with-stages.properties diff --git a/src/site/resources/images/seq-jms.puml b/src/site/resources/images/seq-jms.puml index be63fdf..056ed7a 100644 --- a/src/site/resources/images/seq-jms.puml +++ b/src/site/resources/images/seq-jms.puml @@ -8,21 +8,24 @@ database "SAFRAN database" as safrandb queue "agrometinfo-new-safran-data\nqueue" as queuesafran participant "SEASON-Handler" as handler queue "simulation-done\ntopic" as topicsimulationdone +queue "integration-done\nqueue" as queueintegrationdone database "SEASON database" as seasondb queue "treatment queue" as queuetreatment participant "SEASON-cli" as seasoncli +participant "AgroMetInfo-www" as webapp == Initialization == handler -> queuesafran : subscribe handler -> topicsimulationdone : subscribe +handler -> queueintegrationdone : subscribe seasoncli -> queuetreatment : subscribe == Process SAFRAN == -mf -> agroclim : send SAFRAN data +mf -> agroclim : SAFRAN data from data.gouv agroclim -> safrandb : integrate SAFRAN data agroclim -> queuesafran : publish date of last integrated data @@ -46,5 +49,8 @@ destroy seasoncli == Process results == topicsimulationdone -> handler : notify end of calculation -handler -> seasondb : TODO: handle results from SEASON database +handler -> seasondb : copy results from SEASON database +handler -> queueintegrationdone : notify end of integration +queueintegrationdone -> handler : notify end of integration +handler -> webapp : call empty cache @enduml -- GitLab