Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfix in MvvService. Also added unit tests for ModelsService, BootService and MvvService #65

Merged
merged 1 commit into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
package eu.nebulous.ems.boot;

import com.fasterxml.jackson.databind.ObjectMapper;
import eu.nebulouscloud.exn.core.Publisher;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
Expand All @@ -18,9 +17,11 @@

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;

@Slf4j
@Service
Expand All @@ -35,20 +36,21 @@ public void afterPropertiesSet() throws Exception {
log.info("EMS Boot Service: {}", properties.isEnabled() ? "enabled" : "disabled");
}

void processEmsBootMessage(Command command, String appId, Publisher emsBootResponsePublisher) throws IOException {
String processEmsBootMessage(Command command, String appId, BiConsumer<Map,String> emsBootResponsePublisher) throws IOException {
// Process EMS Boot message
log.info("Received a new EMS Boot message from external broker: {}", command.body());

// Load info from models store
Map<String, String> entry = indexService.getFromIndex(appId);
log.debug("Index entry for app-id: {}, entry: {}", appId, entry);
if (entry==null) {
log.warn("No EMS Boot entry found for app-id: {}", appId);
return;
String msg = "No EMS Boot entry found for app-id: " + appId;
log.warn(msg);
return "ERROR "+msg;
}
String modelFile = entry.get(ModelsService.MODEL_FILE_KEY);
String bindingsFile = entry.get(ModelsService.BINDINGS_FILE_KEY);
String solutionFile = entry.get(ModelsService.SOLUTIONS_FILE_KEY);
String solutionFile = entry.get(ModelsService.SOLUTION_FILE_KEY);
String optimiserMetricsFile = entry.get(ModelsService.OPTIMISER_METRICS_FILE_KEY);
log.info("""
Received EMS Boot request:
Expand All @@ -60,20 +62,24 @@ void processEmsBootMessage(Command command, String appId, Publisher emsBootRespo
""", appId, modelFile, bindingsFile, solutionFile, optimiserMetricsFile);

if (StringUtils.isAnyBlank(appId, modelFile, bindingsFile, optimiserMetricsFile)) {
log.warn("Missing info in EMS Boot entry for app-id: {}", appId);
return;
String msg = "Missing info in EMS Boot entry for app-id: " + appId;
log.warn(msg);
return "ERROR "+msg;
}

String modelStr = Files.readString(Paths.get(properties.getModelsDir(), modelFile));
String modelStr = readFileContentsSafe(modelFile);
log.debug("Model file contents:\n{}", modelStr);
String bindingsStr = Files.readString(Paths.get(properties.getModelsDir(), bindingsFile));
Map bindingsMap = objectMapper.readValue(bindingsStr, Map.class);

String bindingsStr = readFileContentsSafe(bindingsFile);
Map bindingsMap = bindingsStr!=null ? objectMapper.readValue(bindingsStr, Map.class) : Map.of();
log.debug("Bindings file contents:\n{}", bindingsMap);
String solutionStr = Files.readString(Paths.get(properties.getModelsDir(), solutionFile));
Map solutionMap = objectMapper.readValue(solutionStr, Map.class);

String solutionStr = readFileContentsSafe(solutionFile);
Map solutionMap = solutionStr!=null ? objectMapper.readValue(solutionStr, Map.class) : Map.of();
log.debug("Solution file contents:\n{}", solutionMap);
String metricsStr = Files.readString(Paths.get(properties.getModelsDir(), optimiserMetricsFile));
List metricsList = objectMapper.readValue(metricsStr, List.class);

String metricsStr = readFileContentsSafe(optimiserMetricsFile);
List metricsList = metricsStr!=null ? objectMapper.readValue(metricsStr, List.class) : List.of();
log.debug("Optimiser Metrics file contents:\n{}", metricsList);

// Send EMS Boot response message
Expand All @@ -84,7 +90,26 @@ void processEmsBootMessage(Command command, String appId, Publisher emsBootRespo
"solution", solutionMap,
"optimiser-metrics", metricsList
);
emsBootResponsePublisher.send(message, appId);
log.debug("EMS Boot response message: {}", message);
emsBootResponsePublisher.accept(message, appId);
log.info("EMS Boot response sent");
return "OK";
}

protected String readFileContentsSafe(String file) {
if (StringUtils.isBlank(file)) return null;
Path path = Paths.get(properties.getModelsDir(), file);
if (! path.toFile().exists()) {
log.warn("File not found in models dir.: {}", file);
return null;
}

try {
String contents = Files.readString(path);
return contents;
} catch (Exception e) {
log.warn("Error while reading file: {}\n", file, e);
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ public Map<String,String> getAppBindings(@NonNull String appId) throws IOExcepti
}

public Map<String,Double> getAppSolution(@NonNull String appId) throws IOException {
String fileName = getAppData(appId).get(ModelsService.SOLUTIONS_FILE_KEY);
String fileName = getAppData(appId).get(ModelsService.SOLUTION_FILE_KEY);
String solutionStr = applicationContext.getBean(ModelsService.class).readFromFile(fileName);
return objectMapper.readValue(solutionStr, Map.class);
}
Expand Down Expand Up @@ -268,14 +268,16 @@ public synchronized boolean deleteAppsBefore(Instant before, boolean deleteFiles

public synchronized void addAppData(String appId) throws IOException {
long ts = Instant.now().toEpochMilli();
String modelStr, bindingsStr, metricsStr;
String modelStr, bindingsStr, solutionStr, metricsStr;
Map<String, String> data = Map.of(
"model-file", modelStr = appId + "--model--" + ts + ".yml",
"bindings-file", bindingsStr = appId + "--bindings--" + ts + ".json",
"optimiser-metrics-file", metricsStr = appId + "--metrics--" + ts + ".json"
ModelsService.MODEL_FILE_KEY, modelStr = appId + "--model--" + ts + ".yml",
ModelsService.BINDINGS_FILE_KEY, bindingsStr = appId + "--bindings--" + ts + ".json",
ModelsService.SOLUTION_FILE_KEY, solutionStr = appId + "--solution--" + ts + ".json",
ModelsService.OPTIMISER_METRICS_FILE_KEY, metricsStr = appId + "--metrics--" + ts + ".json"
);
Files.writeString(Paths.get(properties.getModelsDir(), modelStr), "{}");
Files.writeString(Paths.get(properties.getModelsDir(), bindingsStr), "{}");
Files.writeString(Paths.get(properties.getModelsDir(), solutionStr), "{}");
Files.writeString(Paths.get(properties.getModelsDir(), metricsStr), "[]");
storeToIndex(appId, data);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@Slf4j
Expand All @@ -35,7 +34,7 @@
public class ModelsService implements InitializingBean {
final static String MODEL_FILE_KEY = "model-file";
final static String BINDINGS_FILE_KEY = "bindings-file";
final static String SOLUTIONS_FILE_KEY = "solutions-file";
final static String SOLUTION_FILE_KEY = "solution-file";
final static String OPTIMISER_METRICS_FILE_KEY = "optimiser-metrics-file";

public final static String SIMPLE_BINDING_KEY = "simple-bindings";
Expand Down Expand Up @@ -140,7 +139,12 @@ String extractBindings(Command command, String appId) throws IOException {

// Store bindings in models store
String bindingsFile = getFileName("bindings", appId, "json");
if (bindingsMap==null) bindingsMap = Map.of();
if (bindingsMap==null) {
bindingsMap = Map.of(
SIMPLE_BINDING_KEY, Map.of(),
COMPOSITE_BINDING_KEY, Map.of()
);
}
storeToFile(bindingsFile, objectMapper.writeValueAsString(bindingsMap));
log.info("Stored bindings in file: app-id={}, file={}", appId, bindingsFile);

Expand Down Expand Up @@ -232,7 +236,7 @@ String extractSolution(Command command, String appId) throws IOException {
log.info("Stored solution in file: app-id={}, file={}", appId, solutionFile);

// Add appId-solutionFile entry in the stored Index
indexService.storeToIndex(appId, Map.of(SOLUTIONS_FILE_KEY, solutionFile));
indexService.storeToIndex(appId, Map.of(SOLUTION_FILE_KEY, solutionFile));

return "OK";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ private void processMessage(Command command) throws ClientException, IOException
sendResponse(modelsResponsePublisher, appId, result);
} else
if (properties.getEmsBootTopic().equals(command.address())) {
bootService.processEmsBootMessage(command, appId, emsBootResponsePublisher);
bootService.processEmsBootMessage(command, appId, (message, app_id) -> emsBootResponsePublisher.send(message, app_id));
} else
log.error("ERROR: Received message from unexpected topic: {}", command.address());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,52 +70,66 @@ public boolean isEmpty() {
}

public void translateAndSetValues(Map<String,Object> varValues) {
log.info("MvvService.translateAndSetValues: New Variable Values: {}", varValues);
Map<String, Double> newValues = new HashMap<>();

// Process simple constants
final Map<String, String> simpleBindings = bindings.get(ModelsService.SIMPLE_BINDING_KEY);
varValues.forEach((k,v) -> {
String constName = simpleBindings.get(k);
if (StringUtils.isNotBlank(constName)) {
if (v instanceof Number n)
newValues.put(constName, n.doubleValue());
else
throw new IllegalArgumentException("Solution variable value is not a number: "+v);
log.trace("MvvService.translateAndSetValues: Added Simple Constant value: {} = {}", constName, n);
} else
log.warn("MvvService.translateAndSetValues: No Constant found for Solution variable: {}", k);
});
log.debug("MvvService.translateAndSetValues: Simple Constant values: {}", newValues);

// Process composite constants
Map<String, String> pending = new HashMap<>(bindings.get(ModelsService.COMPOSITE_BINDING_KEY));
@NotNull final Map<String, Double> varValues1 = varValues.entrySet().stream().collect(Collectors.toMap(
Map.Entry::getKey, e -> ((Number) e.getValue()).doubleValue()
));
while (! pending.isEmpty()) {
Map<String, String> newPending = new HashMap<>();
pending.forEach((formula, constName) -> {
if (StringUtils.isNotBlank(formula) && StringUtils.isNotBlank(constName)) {
try {
log.trace("MvvService.translateAndSetValues: Calculating Composite Constant value: {} ::= {} -- Values: {}", constName, formula, newValues);
Double formulaValue = MathUtil.eval(formula, varValues1);
newValues.put(constName, formulaValue);
log.trace("MvvService.translateAndSetValues: Added Composite Constant value: {} = {}", constName, formulaValue);
} catch (Exception e) {
log.trace("MvvService.translateAndSetValues: Could not calculate Composite Constant value: {} ::= {} -- Values: {} -- Reason: {}", constName, formula, newValues, e.getMessage());
newPending.put(formula, constName);
// Check if bindings are available
if (bindings == null || bindings.isEmpty()) {
log.error("MvvService.translateAndSetValues: No bindings provided: {}", bindings);
return;
}
Map<String, String> simpleBindings = bindings.get(ModelsService.SIMPLE_BINDING_KEY);
Map<String, String> compositeBindings = bindings.get(ModelsService.COMPOSITE_BINDING_KEY);
if (simpleBindings==null || simpleBindings.isEmpty()) {
log.error("MvvService.translateAndSetValues: No simple bindings provided: {}", bindings);
return;
}

// Log new values
log.info("MvvService.translateAndSetValues: New Variable Values: {}", varValues);
Map<String, Double> newValues = new HashMap<>();

// Process simple constants
varValues.forEach((k, v) -> {
String constName = simpleBindings.get(k);
if (StringUtils.isNotBlank(constName)) {
if (v instanceof Number n)
newValues.put(constName, n.doubleValue());
else
throw new IllegalArgumentException("Solution variable value is not a number: " + v);
log.trace("MvvService.translateAndSetValues: Added Simple Constant value: {} = {}", constName, n);
} else
log.warn("MvvService.translateAndSetValues: No Constant found for Solution variable: {}", k);
});
log.debug("MvvService.translateAndSetValues: Simple Constant values: {}", newValues);

// Process composite constants
if (compositeBindings!=null && ! compositeBindings.isEmpty()) {
Map<String, String> pending = new HashMap<>(bindings.get(ModelsService.COMPOSITE_BINDING_KEY));
@NotNull final Map<String, Double> varValues1 = varValues.entrySet().stream().collect(Collectors.toMap(
Map.Entry::getKey, e -> ((Number) e.getValue()).doubleValue()
));
while (!pending.isEmpty()) {
Map<String, String> newPending = new HashMap<>();
pending.forEach((formula, constName) -> {
if (StringUtils.isNotBlank(formula) && StringUtils.isNotBlank(constName)) {
try {
log.trace("MvvService.translateAndSetValues: Calculating Composite Constant value: {} ::= {} -- Values: {}", constName, formula, newValues);
Double formulaValue = MathUtil.eval(formula, varValues1);
newValues.put(constName, formulaValue);
log.trace("MvvService.translateAndSetValues: Added Composite Constant value: {} = {}", constName, formulaValue);
} catch (Exception e) {
log.trace("MvvService.translateAndSetValues: Could not calculate Composite Constant value: {} ::= {} -- Values: {} -- Reason: {}", constName, formula, newValues, e.getMessage());
newPending.put(formula, constName);
}
}
}
});
if (pending.size()==newPending.size())
throw new IllegalArgumentException("Composite Constants cannot be calculated. Check for missing terms or circles in formulas: "+pending);
pending = newPending;
});
if (pending.size() == newPending.size())
throw new IllegalArgumentException("Composite Constants cannot be calculated. Check for missing terms or circles in formulas: " + pending);
pending = newPending;
}
}
log.info("MvvService.translateAndSetValues: New Constant values: {}", newValues);
log.info("MvvService.translateAndSetValues: New Constant values: {}", newValues);

setControlServiceConstants(newValues);
}
setControlServiceConstants(newValues);
}

private void setControlServiceConstants(@NonNull Map<String, Double> newValues) {
this.values = newValues;
Expand Down
Loading