Commit
Change-Id: If19e60cf28d7a597d1589b74b498711551fc02e1
This commit is contained in:
parent
20e0ab5351
commit
93a5d71a6b
13
.gitignore
vendored
13
.gitignore
vendored
@ -1,2 +1,15 @@
|
|||||||
|
/nebulous-automated-tests/.classpath
|
||||||
|
/nebulous-automated-tests/.gitattributes
|
||||||
|
/nebulous-automated-tests/.project
|
||||||
|
/nebulous-automated-tests/.settings/org.eclipse.buildship.core.prefs
|
||||||
|
/nebulous-automated-tests/.settings/org.eclipse.jdt.core.prefs
|
||||||
|
/nebulous-automated-tests/gradlew.bat
|
||||||
|
/nebulous-automated-tests/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
/nebulous-automated-tests/gradle/wrapper/gradle-wrapper.properties
|
||||||
|
/nebulous-automated-tests/gradlew
|
||||||
__pycache__/
|
__pycache__/
|
||||||
.nox/
|
.nox/
|
||||||
|
/nebulous-automated-tests/maven-repo
|
||||||
|
/tests/.settings
|
||||||
|
/tests/logs
|
||||||
|
/apps/mqtt_processor_app/worker/.vscode
|
||||||
|
4
README.md
Normal file
4
README.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
NebulOuS integration tests
|
||||||
|
|
||||||
|
- tests: Folder With a Java Maven project with JUnit tests that validate basic aspects of NebulOuS
|
||||||
|
- apps: Folder containing necessary testing apps used during the testing process.
|
24
apps/mqtt_processor_app/README.md
Normal file
24
apps/mqtt_processor_app/README.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
Source code of the mqtt processor app used for testing.
|
||||||
|
|
||||||
|
On startup, the application connects to the configured MQTT broker and waits for messages on the topic APP_MQTT_INPUT_TOPIC. When a well structured message on said topic, the application simulates some work and sends a message to APP_MQTT_OUTPUT_TOPIC.
|
||||||
|
|
||||||
|
The structure of the input message is:
|
||||||
|
- job_id: An unique UUID assigned to the job
|
||||||
|
- timestamp: Timestamp of the request with the format YYYY-MM-dd HH:mm:ssZ
|
||||||
|
- job_timestamp: Same as timestamp
|
||||||
|
- inference_duration: Time in seconds that the worker processing this job will sleep to simulate a time consuming inference process.
|
||||||
|
|
||||||
|
The worker needs the following environment variables to work:
|
||||||
|
- mqtt_ip: The IP/host of the MQTT broker
|
||||||
|
- mqtt_port: The port of the MQTT broker
|
||||||
|
- mqtt_subscribe_topic: The topic to subscribe to and recieve requests
|
||||||
|
- mqtt_publish_topic: The topic to connect to and publish results
|
||||||
|
|
||||||
|
- report_metrics_to_ems: Flag to indicate if metrics should be published to EMS
|
||||||
|
- nebulous_ems_ip: EMS IP
|
||||||
|
- nebulous_ems_port: EMS port
|
||||||
|
- nebulous_ems_user: EMS user
|
||||||
|
- nebulous_ems_password: EMS password
|
||||||
|
- nebulous_ems_metrics_topic: EMS topic to use to report metrics
|
||||||
|
|
||||||
|
|
7
apps/mqtt_processor_app/worker/Dockerfile
Normal file
7
apps/mqtt_processor_app/worker/Dockerfile
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
FROM python:3.11
|
||||||
|
RUN mkdir /app
|
||||||
|
WORKDIR /app
|
||||||
|
COPY ./requirements.txt /app/requirements.txt
|
||||||
|
RUN pip install -r /app/requirements.txt
|
||||||
|
COPY ./worker.py ./worker.py
|
||||||
|
CMD [ "python3","-u", "./worker.py"]
|
4
apps/mqtt_processor_app/worker/requirements.txt
Normal file
4
apps/mqtt_processor_app/worker/requirements.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
python-dotenv==1.0.0
|
||||||
|
PyYAML==6.0.1
|
||||||
|
paho-mqtt==1.6.1
|
||||||
|
stomp.py
|
128
apps/mqtt_processor_app/worker/worker.py
Normal file
128
apps/mqtt_processor_app/worker/worker.py
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
import stomp
|
||||||
|
import os.path
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
import queue
|
||||||
|
import time
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
from uuid import uuid4
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
print("Starting dummy app worker")
|
||||||
|
|
||||||
|
shared_stack = queue.Queue()
|
||||||
|
worker_id = str(uuid4())
|
||||||
|
# MQTT Broker details
|
||||||
|
mqtt_broker_address = os.getenv("mqtt_ip")
|
||||||
|
mqtt_port = int(os.getenv("mqtt_port"))
|
||||||
|
mqtt_topic = os.getenv("mqtt_subscribe_topic")
|
||||||
|
mqtt_publish_topic = os.getenv("mqtt_publish_topic")
|
||||||
|
|
||||||
|
# STOMP Broker details
|
||||||
|
report_metrics_to_ems = os.getenv("report_metrics_to_ems")
|
||||||
|
stomp_broker_address = os.getenv("nebulous_ems_ip")
|
||||||
|
stomp_port = int(os.getenv("nebulous_ems_port"))
|
||||||
|
stomp_destination = os.getenv("nebulous_ems_metrics_topic")
|
||||||
|
stomp_user = os.getenv("nebulous_ems_user")
|
||||||
|
stomp_pass = os.getenv("nebulous_ems_password")
|
||||||
|
|
||||||
|
def map_value(old_value, old_min, old_max, new_min, new_max):
|
||||||
|
return ( (old_value - old_min) / (old_max - old_min) ) * (new_max - new_min) + new_min
|
||||||
|
|
||||||
|
|
||||||
|
# MQTT callback function
|
||||||
|
def on_message(client, userdata, message):
|
||||||
|
try:
|
||||||
|
payload = message.payload.decode("utf-8")
|
||||||
|
print("Recieved MQTT message",payload)
|
||||||
|
print("Message added to stack. Current length:",shared_stack.qsize())
|
||||||
|
shared_stack.put(payload)
|
||||||
|
backpressure = map_value(min(shared_stack.qsize(),10),0,10,0,2)
|
||||||
|
print("Backpressure: ",backpressure)
|
||||||
|
if backpressure>0:
|
||||||
|
time.sleep(backpressure)
|
||||||
|
except Exception as e:
|
||||||
|
print("Error",e)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def process_messages():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Get message from the shared stack
|
||||||
|
payload = shared_stack.get()
|
||||||
|
payload = json.loads(payload)
|
||||||
|
print("Processing ",payload)
|
||||||
|
print("Proceed to simulate an inference of ",payload["inference_duration"])
|
||||||
|
time.sleep(payload["inference_duration"])
|
||||||
|
date_timestamp = datetime.datetime.strptime(payload['job_timestamp'], "%Y-%m-%d %H:%M:%S%z")
|
||||||
|
total_job_duration = int((datetime.datetime.now(datetime.timezone.utc) - date_timestamp).total_seconds())
|
||||||
|
print(f"total_job_duration: {total_job_duration}")
|
||||||
|
json_msg = {
|
||||||
|
"metricValue": total_job_duration,
|
||||||
|
"level": 1,
|
||||||
|
"timestamp": int(datetime.datetime.now().timestamp())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
payload["worker_id"] = worker_id
|
||||||
|
payload["total_job_duration"] = total_job_duration
|
||||||
|
payload["job_completion_timestamp"] = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S%z")
|
||||||
|
mqtt_client.publish(mqtt_publish_topic,json.dumps(payload),2)
|
||||||
|
|
||||||
|
|
||||||
|
if "True" == report_metrics_to_ems:
|
||||||
|
print("send_metric ",json_msg)
|
||||||
|
print(json.dumps(json_msg))
|
||||||
|
stomp_client.send(body=json.dumps(json_msg), headers={'type':'textMessage', 'amq-msg-type':'text'}, destination=stomp_destination)
|
||||||
|
else:
|
||||||
|
print("EMS reporting is disabled.")
|
||||||
|
except Exception as e:
|
||||||
|
print("Error",e)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# STOMP connection callback
|
||||||
|
def on_connect_stomp():
|
||||||
|
print("Connected to STOMP broker")
|
||||||
|
|
||||||
|
# STOMP error callback
|
||||||
|
def on_error_stomp():
|
||||||
|
print("Error in STOMP connection")
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
print("Connecting to MQTT")
|
||||||
|
# Initialize MQTT client
|
||||||
|
mqtt_client = mqtt.Client()
|
||||||
|
mqtt_client.on_message = on_message
|
||||||
|
mqtt_client.connect(mqtt_broker_address, mqtt_port)
|
||||||
|
mqtt_client.subscribe(mqtt_topic)
|
||||||
|
mqtt_client.enable_logger(logger)
|
||||||
|
publish_thread = threading.Thread(target=process_messages)
|
||||||
|
publish_thread.daemon = True # Daemonize the thread so it will exit when the main thread exits
|
||||||
|
publish_thread.start()
|
||||||
|
print("Done")
|
||||||
|
|
||||||
|
if "True" == report_metrics_to_ems:
|
||||||
|
print("Connecting to STOMP")
|
||||||
|
try:
|
||||||
|
stomp_client = stomp.Connection12(host_and_ports=[(stomp_broker_address, stomp_port)])
|
||||||
|
stomp_client.set_listener('', stomp.PrintingListener())
|
||||||
|
stomp_client.connect(stomp_user, stomp_pass, wait=True)
|
||||||
|
stomp_client.subscribe(stomp_destination,str(uuid4()))
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
mqtt_client.publish(mqtt_publish_topic,"Error in STOMP connection",2)
|
||||||
|
sys.exit(1)
|
||||||
|
print("Done")
|
||||||
|
print("Start MQTT Loop")
|
||||||
|
# Start the MQTT client loop
|
||||||
|
mqtt_client.loop_forever()
|
||||||
|
print("App ended")
|
8
tests/.gitignore
vendored
Normal file
8
tests/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Ignore Gradle project-specific cache directory
|
||||||
|
.gradle
|
||||||
|
|
||||||
|
# Ignore Gradle build output directory
|
||||||
|
build
|
||||||
|
data
|
||||||
|
bin
|
||||||
|
/target/
|
0
tests/README.md
Normal file
0
tests/README.md
Normal file
105
tests/pom.xml
Normal file
105
tests/pom.xml
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>eu.nebulouscloud</groupId>
|
||||||
|
<artifactId>tests</artifactId>
|
||||||
|
<version>0.1.0</version>
|
||||||
|
|
||||||
|
<distributionManagement>
|
||||||
|
<snapshotRepository>
|
||||||
|
<id>ossrh</id>
|
||||||
|
<url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
|
||||||
|
</snapshotRepository>
|
||||||
|
<repository>
|
||||||
|
<id>ossrh</id>
|
||||||
|
<url>
|
||||||
|
https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
|
||||||
|
</repository>
|
||||||
|
</distributionManagement>
|
||||||
|
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>11</maven.compiler.source>
|
||||||
|
<maven.compiler.target>11</maven.compiler.target>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>exn-java-connector</id>
|
||||||
|
<url>https://s01.oss.sonatype.org/content/repositories/snapshots/</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.logging.log4j</groupId>
|
||||||
|
<artifactId>log4j-slf4j-impl</artifactId>
|
||||||
|
<version>2.19.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.jayway.jsonpath</groupId>
|
||||||
|
<artifactId>json-path</artifactId>
|
||||||
|
<version>2.8.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>2.16.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>eu.nebulouscloud</groupId>
|
||||||
|
<artifactId>exn-connector-java</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-simple</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.paho</groupId>
|
||||||
|
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
|
||||||
|
<version>1.2.5</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-simple</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
|
<version>5.8.1</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.3.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
|
||||||
|
</project>
|
55
tests/src/main/resources/log4j2.properties
Normal file
55
tests/src/main/resources/log4j2.properties
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
# contributor license agreements. See the NOTICE file distributed with
|
||||||
|
# this work for additional information regarding copyright ownership.
|
||||||
|
# The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
# (the "License"); you may not use this file except in compliance with
|
||||||
|
# the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# Log4J 2 configuration
|
||||||
|
|
||||||
|
# Monitor config file every X seconds for updates
|
||||||
|
monitorInterval = 5
|
||||||
|
|
||||||
|
loggers = activemq
|
||||||
|
logger.activemq.name = org.apache.activemq
|
||||||
|
logger.activemq.level = WARN
|
||||||
|
|
||||||
|
|
||||||
|
loggers = test
|
||||||
|
logger.test.name = eut.nebulouscloud.automated_tests
|
||||||
|
logger.test.level = DEBUG
|
||||||
|
|
||||||
|
#logger.org.apache.activemq.artemis.core.server.cluster.level = TRACE
|
||||||
|
|
||||||
|
rootLogger.level = INFO
|
||||||
|
#rootLogger = console
|
||||||
|
rootLogger.appenderRef.console.ref = console
|
||||||
|
appenders = console, file
|
||||||
|
|
||||||
|
appender.file.type = File
|
||||||
|
appender.file.name = LOGFILE
|
||||||
|
appender.file.fileName=logs/log4j.log
|
||||||
|
appender.file.layout.type=PatternLayout
|
||||||
|
appender.file.layout.pattern=[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n
|
||||||
|
#appender.file.filter.threshold.type = ThresholdFilter
|
||||||
|
#appender.file.filter.threshold.level = info
|
||||||
|
|
||||||
|
|
||||||
|
# Console appender
|
||||||
|
appender.console.type = Console
|
||||||
|
appender.console.name = STDOUT
|
||||||
|
appender.console.layout.type = PatternLayout
|
||||||
|
appender.console.layout.pattern = [%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n
|
||||||
|
|
||||||
|
|
||||||
|
rootLogger.appenderRefs = stdout, logfile
|
||||||
|
rootLogger.appenderRef.stdout.ref = STDOUT
|
||||||
|
rootLogger.appenderRef.logfile.ref = LOGFILE
|
@ -0,0 +1,74 @@
|
|||||||
|
package eut.nebulouscloud.tests;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
public class FileTemplatingUtils {
|
||||||
|
static Logger LOGGER = LoggerFactory.getLogger(FileTemplatingUtils.class);
|
||||||
|
static ObjectMapper om = new ObjectMapper();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a JSON file stored in the resources folder of the project and perform the substitutions provided.
|
||||||
|
* @param path
|
||||||
|
* @param substitutions
|
||||||
|
* @return
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public static Map<String,Object> loadJSONFileAndSubstitute(String path,Map<String,String> substitutions) throws Exception
|
||||||
|
{
|
||||||
|
return om.readValue(loadFileAndSubstitute(path, substitutions),HashMap.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a text file stored in the resources folder of the project and perform the substitutions provided.
|
||||||
|
* @param path
|
||||||
|
* @param substitutions
|
||||||
|
* @return
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public static String loadFileAndSubstitute(String path,Map<String,String> substitutions) throws Exception
|
||||||
|
{
|
||||||
|
StringBuilder contentBuilder = new StringBuilder();
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(FileTemplatingUtils.class.getClassLoader()
|
||||||
|
.getResourceAsStream(path)))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
// Apply substitutions
|
||||||
|
line = applySubstitutions(line, substitutions);
|
||||||
|
contentBuilder.append(line).append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception(ex);
|
||||||
|
}
|
||||||
|
return contentBuilder.toString();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find any placeholders in the given line and substitute them with the appropriate value
|
||||||
|
* @param line
|
||||||
|
* @param substitutions
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private static String applySubstitutions(String line, Map<String, String> substitutions) {
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> entry : substitutions.entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
String value = entry.getValue();
|
||||||
|
line = line.replace(key, value);
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,293 @@
|
|||||||
|
package eut.nebulouscloud.tests;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.testng.annotations.AfterTest;
|
||||||
|
import org.testng.annotations.BeforeTest;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
class MQTTProcessorAppDeploymentTest {
|
||||||
|
static Logger LOGGER = LoggerFactory.getLogger(MQTTProcessorAppDeploymentTest.class);
|
||||||
|
protected ObjectMapper om = new ObjectMapper();
|
||||||
|
static int DELAY_SECONDS = 3;
|
||||||
|
|
||||||
|
String applicationId = new SimpleDateFormat("HHmmssddMM").format(new Date())
|
||||||
|
+ "automated-testing-mqtt-app-"
|
||||||
|
+ new Date().getTime();
|
||||||
|
|
||||||
|
NebulousCoreMessageBrokerInterface coreBroker;
|
||||||
|
MQTTProcessorAppMessageBrokerInterface appBroker;
|
||||||
|
String mqttBroker = "broker.emqx.io";
|
||||||
|
String mqttPort = "1883";
|
||||||
|
String mqttTopicPrefix = "atest";
|
||||||
|
String mqttAppInputTopic = mqttTopicPrefix + "/input";
|
||||||
|
String mqttAppOutputTopic = mqttTopicPrefix + "/output";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This test ensures that a MQTT processor app can be deployed using NebulOuS. The test
|
||||||
|
* simulates the user requesting a deployment of an app through UI (sending app
|
||||||
|
* creation message and metric model). Then, the test asserts that optimizer
|
||||||
|
* controller performs expected actions, namely: - requesting node candidates
|
||||||
|
* and geting a response from CFSB - defines the app cluster - deploys the app
|
||||||
|
* cluster - sends the AMPL file for the solver - reports app status to be
|
||||||
|
* running
|
||||||
|
*
|
||||||
|
* Once the optimizer controller reports the app being successfully deployed,
|
||||||
|
* the test asserts that the app works as expected. For this, it connects to a
|
||||||
|
* public MQTT broker where the app is waiting for job requests, publish one and waits for the response
|
||||||
|
* for them.
|
||||||
|
*
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void test() throws Exception {
|
||||||
|
|
||||||
|
LOGGER.info(String.format("Begin MQTT Processor APP deployment. applicationId is %s", applicationId));
|
||||||
|
coreBroker = new NebulousCoreMessageBrokerInterface();
|
||||||
|
appBroker = new MQTTProcessorAppMessageBrokerInterface("tcp://" + mqttBroker + ":" + mqttPort, mqttAppOutputTopic);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare and send app creation message and assert is correctly received by any subscriber.
|
||||||
|
*
|
||||||
|
* The app creation message payload template is stored in the project resources folder. This file contains several
|
||||||
|
* parameters that need to be substituted. These are:
|
||||||
|
* APP_ID: The id of the app being deployed
|
||||||
|
* MQTT connection details (APP_MQTT_BROKER_SERVER, APP_MQTT_BROKER_PORT, APP_MQTT_INPUT_TOPIC, APP_MQTT_OUTPUT_TOPIC): On startup, the application connects to the configured MQTT broker
|
||||||
|
* and waits for messages on the topic APP_MQTT_INPUT_TOPIC. Uppon a well structured message on said topic, the application simulates some work and sends a message to APP_MQTT_OUTPUT_TOPIC.
|
||||||
|
* REPORT_METRICS_TO_EMS: If true, application tries to connect to local EMS broker to report metrics. If false, not.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
LOGGER.info("send app creation message");
|
||||||
|
|
||||||
|
Map<String, String> appParameters = new HashMap<String, String>();
|
||||||
|
appParameters.put("{{APP_ID}}", applicationId);
|
||||||
|
appParameters.put("{{APP_MQTT_BROKER_SERVER}}", mqttBroker);
|
||||||
|
appParameters.put("{{APP_MQTT_BROKER_PORT}}", mqttPort);
|
||||||
|
appParameters.put("{{APP_MQTT_INPUT_TOPIC}}", "$share/workers/" + mqttAppInputTopic);
|
||||||
|
appParameters.put("{{APP_MQTT_OUTPUT_TOPIC}}", mqttAppOutputTopic);
|
||||||
|
appParameters.put("{{REPORT_METRICS_TO_EMS}}", "True");
|
||||||
|
|
||||||
|
Map<String, Object> appCreationPayload = FileTemplatingUtils
|
||||||
|
.loadJSONFileAndSubstitute("mqtt_processor_app/app_creation_message.json", appParameters);
|
||||||
|
coreBroker.sendAppCreationMessage(appCreationPayload, applicationId);
|
||||||
|
|
||||||
|
// Assert that the message was sent
|
||||||
|
assertTrue(coreBroker.findFirst(applicationId, "eu.nebulouscloud.ui.dsl.generic", null, 10).isPresent());
|
||||||
|
Thread.sleep(DELAY_SECONDS * 1000);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send metric model and assert is correctly received by any subscriber
|
||||||
|
*/
|
||||||
|
LOGGER.info("send metric model");
|
||||||
|
Map<String, Object> metricModelPayload = FileTemplatingUtils.loadJSONFileAndSubstitute("mqtt_processor_app/metric_model.json",
|
||||||
|
Map.of("{{APP_ID}}", applicationId));
|
||||||
|
coreBroker.sendMetricModelMessage(metricModelPayload, applicationId);
|
||||||
|
assertTrue(coreBroker.findFirst(applicationId, "eu.nebulouscloud.ui.dsl.metric_model", null, 10).isPresent());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that Optimizer controller requests for node candidates for the
|
||||||
|
* application cluster
|
||||||
|
*/
|
||||||
|
LOGGER.info("Wait for optimizer to request node candidates");
|
||||||
|
|
||||||
|
Optional<NebulOuSCoreMessage> nodeRequestToCFSB = coreBroker.findFirst(applicationId,
|
||||||
|
"eu.nebulouscloud.cfsb.get_node_candidates", null, 10);
|
||||||
|
assertTrue(nodeRequestToCFSB.isPresent());
|
||||||
|
assertNotNull(nodeRequestToCFSB.get().correlationId);
|
||||||
|
|
||||||
|
Optional<NebulOuSCoreMessage> nodeRequestToSAL = coreBroker.findFirst(applicationId,
|
||||||
|
"eu.nebulouscloud.exn.sal.nodecandidate.get", null, 10);
|
||||||
|
assertTrue(nodeRequestToSAL.isPresent());
|
||||||
|
assertNotNull(nodeRequestToSAL.get().correlationId);
|
||||||
|
/**
|
||||||
|
* Assert that SAL anwsers the request
|
||||||
|
*/
|
||||||
|
LOGGER.info("Wait for CFSB to recieve an answer on node candidates from SAL");
|
||||||
|
assertTrue(coreBroker.findFirst(applicationId, "eu.nebulouscloud.exn.sal.nodecandidate.get.reply",
|
||||||
|
m -> nodeRequestToSAL.get().correlationId.equals(m.correlationId), 30).isPresent());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that CFSB anwsers the request
|
||||||
|
*/
|
||||||
|
LOGGER.info("Wait for optimizer to recieve an answer on node candidates from CFSB");
|
||||||
|
assertTrue(coreBroker.findFirst(applicationId, "eu.nebulouscloud.cfsb.get_node_candidates.reply",
|
||||||
|
m -> nodeRequestToCFSB.get().correlationId.equals(m.correlationId), 30).isPresent());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that optimiser defines the cluster
|
||||||
|
*/
|
||||||
|
LOGGER.info("Wait for optimizer to define cluster");
|
||||||
|
Optional<NebulOuSCoreMessage> defineClusterRequest = coreBroker.findFirst(applicationId,
|
||||||
|
"eu.nebulouscloud.exn.sal.cluster.define", null, 80);
|
||||||
|
assertTrue(defineClusterRequest.isPresent());
|
||||||
|
LOGGER.info(om.writeValueAsString(defineClusterRequest.get().payload));
|
||||||
|
// Retrieve the name of the new cluster
|
||||||
|
String clusterName = (String) om
|
||||||
|
.readValue((String) defineClusterRequest.get().payload.get("body"), HashMap.class).get("name");
|
||||||
|
|
||||||
|
LOGGER.info(String.format("Cluster name: %s", clusterName));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that Optimiser deploys the cluster
|
||||||
|
*/
|
||||||
|
LOGGER.info("Wait for optimizer to deploy cluster");
|
||||||
|
assertTrue(
|
||||||
|
coreBroker.findFirst(applicationId, "eu.nebulouscloud.exn.sal.cluster.deploy", null, 80).isPresent());
|
||||||
|
|
||||||
|
LOGGER.info("Wait for a message from optimizer controller to solver with the AMPL File");
|
||||||
|
assertTrue(
|
||||||
|
coreBroker.findFirst(applicationId, "eu.nebulouscloud.exn.sal.cluster.deploy", null, 80).isPresent());
|
||||||
|
|
||||||
|
LOGGER.info("Wait for cluster to be ready");
|
||||||
|
waitForCluster(clusterName, 60 * 10);
|
||||||
|
|
||||||
|
LOGGER.info("Wait for APP state to be Running");
|
||||||
|
assertTrue(waitForAppRunning(60 * 10));
|
||||||
|
|
||||||
|
LOGGER.info("Wait for APP to be operative");
|
||||||
|
assertTrue(checkApplicationWorks(60 * 10));
|
||||||
|
// myEXNClient.stop();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that the application is working by sending an input message through
|
||||||
|
* the app message broker and expecting the apropriate answer from the
|
||||||
|
* application throught the same app message broker. If the application reports
|
||||||
|
* a problem with STOMP communication for publishing metrics to EMS "Error in
|
||||||
|
* STOMP connection", retry 2 times and give up.
|
||||||
|
*
|
||||||
|
* @param timeoutSeconds The ammount of seconds to wait for an answer
|
||||||
|
* @return true if the application responded, false otherwise
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
private boolean checkApplicationWorks(int timeoutSeconds) throws Exception {
|
||||||
|
long timeout = new Date().getTime() + (timeoutSeconds * 1000);
|
||||||
|
int retriesLeft = 2;
|
||||||
|
do {
|
||||||
|
/**
|
||||||
|
* Build a request to be sent to the application input topic.
|
||||||
|
*/
|
||||||
|
Map<String, Object> inferenceRequest = new HashMap<String, Object>();
|
||||||
|
inferenceRequest.put("timestamp", new SimpleDateFormat("YYYY-MM-dd HH:mm:ssZ").format(new Date()));
|
||||||
|
inferenceRequest.put("job_timestamp", inferenceRequest.get("timestamp"));
|
||||||
|
inferenceRequest.put("inference_duration", 1);
|
||||||
|
String jobId = UUID.randomUUID().toString();
|
||||||
|
inferenceRequest.put("job_id", jobId);
|
||||||
|
String payload = om.writeValueAsString(inferenceRequest);
|
||||||
|
// Send the request
|
||||||
|
appBroker.publish(mqttAppInputTopic, payload);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the application sends a message to the response channel with
|
||||||
|
* apropriate structure (check it is a JSON and has the job_id value). If found,
|
||||||
|
* we can consider the app is running
|
||||||
|
*/
|
||||||
|
if (appBroker.findFirst(m -> {
|
||||||
|
return m.jsonPayload() != null && jobId.equals(m.jsonPayload().get("job_id"));
|
||||||
|
}, 3).isPresent()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there is a message with the content "Error in STOMP connection" it means
|
||||||
|
* that the APP is not able to publish metrics to EMS using STOMP. In this
|
||||||
|
* situation, retry at most two times.
|
||||||
|
*/
|
||||||
|
if (appBroker.findFirst(m -> "Error in STOMP connection".equals(m.payload), 3).isPresent()) {
|
||||||
|
retriesLeft--;
|
||||||
|
LOGGER.error("APP is reporting initialization error. Retries left:" + retriesLeft);
|
||||||
|
appBroker.clearMessageCache();
|
||||||
|
if (retriesLeft == 0)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} while (new Date().getTime() < timeout);
|
||||||
|
LOGGER.error("Timeout waiting for a message");
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean waitForCluster(String clusterName, int timeoutSeconds) {
|
||||||
|
long timeout = new Date().getTime() + (timeoutSeconds * 1000);
|
||||||
|
do {
|
||||||
|
String status = coreBroker.getClusterStatus(clusterName);
|
||||||
|
if (status == null || "submited".equals(status)) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(10000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// TODO Auto-generated catch block
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ("deployed".equals(status)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} while (new Date().getTime() < timeout);
|
||||||
|
LOGGER.error("Timeout waiting for a message");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for the optimizer controller to report that the application is on status "RUNNING" (return true) or "FAILED" (return false).
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>NEW: The application has been created from the GUI and is waiting for the
|
||||||
|
* performance indicators from the utility evaluator. *
|
||||||
|
* <li>READY: The application is ready for deployment.
|
||||||
|
* <li>DEPLOYING: The application is being deployed or redeployed.
|
||||||
|
* <li>RUNNING: The application is running.
|
||||||
|
* <li>FAILED: The application is in an invalid state: one or more messages
|
||||||
|
* could not be parsed, or deployment or redeployment failed.
|
||||||
|
*
|
||||||
|
* @param timeoutSeconds
|
||||||
|
* @return True if the optimizer controller reported the app to be running, false if the optimizer controller the app to have failed or the timeout is reached.
|
||||||
|
*/
|
||||||
|
private boolean waitForAppRunning(int timeoutSeconds) {
|
||||||
|
|
||||||
|
long timeout = new Date().getTime() + (timeoutSeconds * 1000);
|
||||||
|
do {
|
||||||
|
/**
|
||||||
|
* Check if app status is reported to be running
|
||||||
|
*/
|
||||||
|
if (coreBroker.findFirst(applicationId, "eu.nebulouscloud.optimiser.controller.app_state",
|
||||||
|
m -> "RUNNING".equals(m.payload.get("state")), 2).isPresent()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Check if APP status is failed.
|
||||||
|
*/
|
||||||
|
if (coreBroker.findFirst(applicationId, "eu.nebulouscloud.optimiser.controller.app_state",
|
||||||
|
m -> "FAILED".equals(m.payload.get("state")), 2).isPresent()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(10000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
} while (new Date().getTime() < timeout);
|
||||||
|
LOGGER.error("Timeout waiting for a message");
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,154 @@
|
|||||||
|
package eut.nebulouscloud.tests;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import org.eclipse.paho.client.mqttv3.*;
|
||||||
|
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
/***
|
||||||
|
* Class for facilitating the interaction with the MQTT broker used by the MQTT processing APP.
|
||||||
|
* Registers any received message and offers methods to query them.
|
||||||
|
* Implements a method for facilitating sending messages to the processing app using MQTT.
|
||||||
|
*/
|
||||||
|
public class MQTTProcessorAppMessageBrokerInterface {
|
||||||
|
|
||||||
|
static Logger LOGGER = LoggerFactory.getLogger(MQTTProcessorAppMessageBrokerInterface.class);
|
||||||
|
private final AtomicBoolean messageRecieved = new AtomicBoolean(false);
|
||||||
|
MqttClient client;
|
||||||
|
protected ObjectMapper om = new ObjectMapper();
|
||||||
|
private List<SimpleMQTTMessage> messages = Collections.synchronizedList(new LinkedList<SimpleMQTTMessage>());
|
||||||
|
|
||||||
|
public class SimpleMQTTMessage {
|
||||||
|
final protected ObjectMapper om = new ObjectMapper();
|
||||||
|
final String topic;
|
||||||
|
final String payload;
|
||||||
|
final Date date;
|
||||||
|
|
||||||
|
public SimpleMQTTMessage(String topic, String payload) {
|
||||||
|
super();
|
||||||
|
this.topic = topic;
|
||||||
|
this.payload = payload;
|
||||||
|
this.date = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> jsonPayload() {
|
||||||
|
try {
|
||||||
|
return om.readValue(payload, HashMap.class);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
// TODO Auto-generated catch block
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publish(String topic,String payload)
|
||||||
|
{
|
||||||
|
MqttMessage m = new MqttMessage();
|
||||||
|
m.setQos(2);
|
||||||
|
m.setPayload(payload.getBytes());
|
||||||
|
try {
|
||||||
|
client.publish(topic, m);
|
||||||
|
LOGGER.info("Message published: "+payload);
|
||||||
|
} catch (MqttException e) {
|
||||||
|
// TODO Auto-generated catch block
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public MQTTProcessorAppMessageBrokerInterface(String broker, String baseTopic) {
|
||||||
|
try {
|
||||||
|
LOGGER.info("Connecting to broker: " + broker);
|
||||||
|
client = new MqttClient(broker, MqttClient.generateClientId(), new MemoryPersistence());
|
||||||
|
MqttConnectOptions connOpts = new MqttConnectOptions();
|
||||||
|
connOpts.setCleanSession(true);
|
||||||
|
|
||||||
|
client.connect(connOpts);
|
||||||
|
LOGGER.info("Connected");
|
||||||
|
|
||||||
|
client.setCallback(new MqttCallback() {
|
||||||
|
@Override
|
||||||
|
public void connectionLost(Throwable throwable) {
|
||||||
|
LOGGER.error("Connection lost!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void messageArrived(String topic, MqttMessage message) throws Exception {
|
||||||
|
String payload = new String(message.getPayload());
|
||||||
|
LOGGER.info("Message received: " + payload);
|
||||||
|
messages.add(new SimpleMQTTMessage(topic, payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
client.subscribe(baseTopic+"/#");
|
||||||
|
} catch (MqttException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for a message that matches the given predicate to appear and returns it
|
||||||
|
* (if found). If timeout is reached without the message being recieved, returns
|
||||||
|
* an empty optional.
|
||||||
|
*
|
||||||
|
* @param predicate The search predicate. If null, it is not used.
|
||||||
|
* @param timeoutSeconds The maximum timeout to wait for a message with the
|
||||||
|
* given predicate to be found in the list (in seconds).
|
||||||
|
* It must be a positive integer or 0.
|
||||||
|
* @return An optional with the first message that matchs the predicate if any
|
||||||
|
* found.
|
||||||
|
*/
|
||||||
|
public Optional<SimpleMQTTMessage> findFirst(Predicate<SimpleMQTTMessage> predicate, int timeoutSeconds) {
|
||||||
|
Optional<SimpleMQTTMessage> result = Optional.empty();
|
||||||
|
long timeout = new Date().getTime() + (timeoutSeconds * 1000);
|
||||||
|
do {
|
||||||
|
synchronized (messages) {
|
||||||
|
result = messages.stream().filter(predicate).findFirst();
|
||||||
|
}
|
||||||
|
if (result.isEmpty() && new Date().getTime() < timeout) {
|
||||||
|
LOGGER.error(String.format("Waiting for message. %.2fs left for timeout.",
|
||||||
|
((timeout - new Date().getTime()) / 1000.0)));
|
||||||
|
try {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (result.isEmpty() && new Date().getTime() < timeout);
|
||||||
|
if (new Date().getTime() > timeout) {
|
||||||
|
LOGGER.error("Timeout waiting for a message");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all messages from the cache
|
||||||
|
*/
|
||||||
|
public void clearMessageCache()
|
||||||
|
{
|
||||||
|
synchronized (messages) {
|
||||||
|
messages.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package eut.nebulouscloud.tests;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that represents a message sent through the NebulOuS message broker.
|
||||||
|
* The prefix "topic://" is removed from the topic value (if exists).
|
||||||
|
*/
|
||||||
|
public class NebulOuSCoreMessage {
|
||||||
|
public Date receptionDate;
|
||||||
|
public String topic;
|
||||||
|
public Map<String,Object> payload;
|
||||||
|
public String applicationId;
|
||||||
|
public String correlationId;
|
||||||
|
public NebulOuSCoreMessage(Date receptionDate, String topic, Map<String, Object> payload, String applicationId,
|
||||||
|
String correlationId) {
|
||||||
|
super();
|
||||||
|
this.receptionDate = receptionDate;
|
||||||
|
this.topic = topic!=null?topic.replaceFirst("topic://", ""):null;
|
||||||
|
this.payload = payload;
|
||||||
|
this.applicationId = applicationId;
|
||||||
|
this.correlationId = correlationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,286 @@
|
|||||||
|
package eut.nebulouscloud.tests;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import org.apache.qpid.protonj2.client.Message;
|
||||||
|
import org.apache.qpid.protonj2.client.exceptions.ClientException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import eu.nebulouscloud.exn.Connector;
|
||||||
|
import eu.nebulouscloud.exn.core.Consumer;
|
||||||
|
import eu.nebulouscloud.exn.core.Context;
|
||||||
|
import eu.nebulouscloud.exn.core.Handler;
|
||||||
|
import eu.nebulouscloud.exn.core.Publisher;
|
||||||
|
import eu.nebulouscloud.exn.core.SyncedPublisher;
|
||||||
|
import eu.nebulouscloud.exn.handlers.ConnectorHandler;
|
||||||
|
import eu.nebulouscloud.exn.settings.StaticExnConfig;
|
||||||
|
|
||||||
|
/***
|
||||||
|
* Class for facilitating the interaction with the NebulOuS message broker with connection parameters host:"localhost", port: 5672, user: "admin", password:"admin".
|
||||||
|
* Implements several functions to send messages to mimic the behaviour of certain NebulOuS components.
|
||||||
|
* Registers any received message and offers methods to query them.
|
||||||
|
*/
|
||||||
|
public class NebulousCoreMessageBrokerInterface {
|
||||||
|
static Logger LOGGER = LoggerFactory.getLogger(NebulousCoreMessageBrokerInterface.class);
|
||||||
|
protected ObjectMapper om = new ObjectMapper();
|
||||||
|
Publisher metricModelPublisher;
|
||||||
|
Publisher appDeployMessagePublisher;
|
||||||
|
SyncedPublisher getClusterPublisher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broker connection properties
|
||||||
|
*/
|
||||||
|
final String brokerHost = "localhost";
|
||||||
|
final int brokerPort = 5672;
|
||||||
|
final String brokerUser = "admin";
|
||||||
|
final String brokerPassword ="admin";
|
||||||
|
|
||||||
|
private List<NebulOuSCoreMessage> messages = Collections.synchronizedList(new LinkedList<NebulOuSCoreMessage>());
|
||||||
|
|
||||||
|
|
||||||
|
public NebulousCoreMessageBrokerInterface() {
|
||||||
|
/**
|
||||||
|
* Setup NebulOuS message broker client
|
||||||
|
*/
|
||||||
|
LOGGER.info("Start NebulOuS message broker client");
|
||||||
|
metricModelPublisher = new Publisher("metricModelPublisher", "eu.nebulouscloud.ui.dsl.metric_model", true,
|
||||||
|
true);
|
||||||
|
appDeployMessagePublisher = new Publisher("appDeployMessagePublisher", "eu.nebulouscloud.ui.dsl.generic", true,
|
||||||
|
true);
|
||||||
|
getClusterPublisher = new SyncedPublisher("getCluster", "eu.nebulouscloud.exn.sal.cluster.get", true, true);
|
||||||
|
Consumer cons1 = new Consumer("monitoring", ">", new MyConsumerHandler(this), true, true);
|
||||||
|
Connector myEXNClient = new Connector("thisINotImportant", new MyConnectorHandler(),
|
||||||
|
List.of(metricModelPublisher, appDeployMessagePublisher, getClusterPublisher), List.of(cons1), true,
|
||||||
|
true,
|
||||||
|
new StaticExnConfig(brokerHost, brokerPort, brokerUser, brokerPassword));
|
||||||
|
myEXNClient.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for a message that matches the given predicate to appear and returns it
|
||||||
|
* (if found). If timeout is reached without the message being recieved, returns
|
||||||
|
* an empty optional.
|
||||||
|
*
|
||||||
|
* @param appId the app Id to filter by. If null, no filtering occurs
|
||||||
|
* by appId
|
||||||
|
* @param topic the topic to filter by. If null, no filtering occurs by
|
||||||
|
* topic
|
||||||
|
* @param predicate The search predicate. If null, it is not used.
|
||||||
|
* @param timeoutSeconds The maximum timeout to wait for a message with the
|
||||||
|
* given predicate to be found in the list (in seconds).
|
||||||
|
* It must be a positive integer or 0.
|
||||||
|
* @return An optional with the first message that matchs the predicate if any
|
||||||
|
* found.
|
||||||
|
*/
|
||||||
|
public Optional<NebulOuSCoreMessage> findFirst(String appId, String topic, Predicate<NebulOuSCoreMessage> predicate,
|
||||||
|
int timeoutSeconds) {
|
||||||
|
Optional<NebulOuSCoreMessage> result = Optional.empty();
|
||||||
|
long timeout = new Date().getTime() + (timeoutSeconds * 1000);
|
||||||
|
Predicate<NebulOuSCoreMessage> finalPredicate = predicate != null
|
||||||
|
? messagesFromAppAndTopic(appId, topic).and(predicate)
|
||||||
|
: messagesFromAppAndTopic(appId, topic);
|
||||||
|
do {
|
||||||
|
synchronized (messages) {
|
||||||
|
|
||||||
|
result = messages.stream().filter(finalPredicate).findFirst();
|
||||||
|
}
|
||||||
|
if (result.isEmpty() && new Date().getTime() < timeout) {
|
||||||
|
LOGGER.error(String.format("Waiting for message. %.2fs left for timeout.",
|
||||||
|
((timeout - new Date().getTime()) / 1000.0)));
|
||||||
|
try {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (result.isEmpty() && new Date().getTime() < timeout);
|
||||||
|
if (new Date().getTime() > timeout) {
|
||||||
|
LOGGER.error("Timeout waiting for a message");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
class MyConsumerHandler extends Handler {
|
||||||
|
NebulousCoreMessageBrokerInterface messageStore;
|
||||||
|
|
||||||
|
public MyConsumerHandler(NebulousCoreMessageBrokerInterface messageStore) {
|
||||||
|
this.messageStore = messageStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(String key, String address, Map body, Message message, Context context) {
|
||||||
|
String to = "??";
|
||||||
|
try {
|
||||||
|
to = message.to() != null ? message.to() : address;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
}
|
||||||
|
Map<Object, Object> props = new HashMap<Object, Object>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
message.forEachProperty((k, v) -> props.put(k, v));
|
||||||
|
} catch (ClientException e) {
|
||||||
|
// TODO Auto-generated catch block
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
String subject = "?";
|
||||||
|
try {
|
||||||
|
subject = message.subject();
|
||||||
|
} catch (ClientException e) {
|
||||||
|
// TODO Auto-generated catch block
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
Object correlationId = 0;
|
||||||
|
try {
|
||||||
|
correlationId = message.correlationId();
|
||||||
|
} catch (ClientException e) {
|
||||||
|
// TODO Auto-generated catch block
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.trace("\r\n{}\r\nsubject:{}\r\npayload:{}\r\nproperties:{}\r\ncorrelationId:{}", to, subject, body,
|
||||||
|
props, correlationId);
|
||||||
|
|
||||||
|
NebulOuSCoreMessage internal = new NebulOuSCoreMessage(new Date(), to, body,
|
||||||
|
(String) props.getOrDefault("application", null),
|
||||||
|
correlationId != null ? correlationId.toString() : "");
|
||||||
|
messageStore.add(internal);
|
||||||
|
try {
|
||||||
|
LOGGER.trace(om.writeValueAsString(body));
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries SAL for the status of a cluster
|
||||||
|
* @param clusterName The cluster name.
|
||||||
|
* @return The cluster status, or null in case of error.
|
||||||
|
*/
|
||||||
|
public String getClusterStatus(String clusterName) {
|
||||||
|
Map<String, Object> msg = Map.of("metaData", Map.of("user", "admin", "clusterName", clusterName));
|
||||||
|
Map<String, Object> response = getClusterPublisher.sendSync(msg, clusterName, null, false);
|
||||||
|
JsonNode payload = extractPayloadFromExnResponse(response, "getCluster");
|
||||||
|
if (payload.isMissingNode())
|
||||||
|
return null;
|
||||||
|
LOGGER.info("isClusterReady: " + payload.toString());
|
||||||
|
JsonNode jsonState = payload.at("/status");
|
||||||
|
if (jsonState.isMissingNode())
|
||||||
|
return null;
|
||||||
|
return jsonState.asText();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract and check the SAL response from an exn-middleware response. The SAL
|
||||||
|
* response will be valid JSON encoded as a string in the "body" field of the
|
||||||
|
* response. If the response is of the following form, log an error and return a
|
||||||
|
* missing node instead:
|
||||||
|
*
|
||||||
|
* <pre>{@code
|
||||||
|
* {
|
||||||
|
* "key": <known exception key>,
|
||||||
|
* "message": "some error message"
|
||||||
|
* }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @param responseMessage The response from exn-middleware.
|
||||||
|
* @param caller Caller information, used for logging only.
|
||||||
|
* @return The SAL response as a parsed JsonNode, or a node where {@code
|
||||||
|
* isMissingNode()} will return true if SAL reported an error.
|
||||||
|
*/
|
||||||
|
private JsonNode extractPayloadFromExnResponse(Map<String, Object> responseMessage, String caller) {
|
||||||
|
JsonNode response = om.valueToTree(responseMessage);
|
||||||
|
String salRawResponse = response.at("/body").asText(); // it's already a string, asText() is for the type system
|
||||||
|
JsonNode metadata = response.at("/metaData");
|
||||||
|
JsonNode salResponse = om.missingNode(); // the data coming from SAL
|
||||||
|
try {
|
||||||
|
salResponse = om.readTree(salRawResponse);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
LOGGER.error("Could not read message body as JSON: body = '{}', caller = '{}'", salRawResponse, caller, e);
|
||||||
|
return om.missingNode();
|
||||||
|
}
|
||||||
|
if (!metadata.at("/status").asText().startsWith("2")) {
|
||||||
|
// we only accept 200, 202, numbers of that nature
|
||||||
|
LOGGER.error("exn-middleware-sal request failed with error code '{}' and message '{}', caller '{}'",
|
||||||
|
metadata.at("/status"), salResponse.at("/message").asText(), caller);
|
||||||
|
return om.missingNode();
|
||||||
|
}
|
||||||
|
return salResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MyConnectorHandler extends ConnectorHandler {
|
||||||
|
|
||||||
|
public void onReady(AtomicReference<Context> context) {
|
||||||
|
LOGGER.info("Optimiser-controller connected to ActiveMQ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a predicate that filters by messages for a certain appId and topic
|
||||||
|
* @param appId: The appId to filter for. If null, it is ignored
|
||||||
|
* @param topic: The topic to filter for. If null, it is ignored.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static Predicate<NebulOuSCoreMessage> messagesFromAppAndTopic(String appId, String topic) {
|
||||||
|
return messagesFromApp(appId).and((Predicate<NebulOuSCoreMessage>) m -> topic == null || topic.equals(m.topic));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a predicate that filters messages for the given app ID. If appID is null, the predicate has no effect.
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static Predicate<NebulOuSCoreMessage> messagesFromApp(String id) {
|
||||||
|
return ((Predicate<NebulOuSCoreMessage>) m -> id == null || id.equals(m.applicationId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new message to the cache
|
||||||
|
*
|
||||||
|
* @param message
|
||||||
|
*/
|
||||||
|
public void add(NebulOuSCoreMessage message) {
|
||||||
|
try {
|
||||||
|
LOGGER.trace("Adding message:" + om.writeValueAsString(message));
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
// TODO Auto-generated catch block
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
this.messages.add(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an app creation message as done by the UI during deployment.
|
||||||
|
* @param appCreationPayload
|
||||||
|
* @param applicationId
|
||||||
|
*/
|
||||||
|
public void sendAppCreationMessage(Map<String, Object> appCreationPayload, String applicationId) {
|
||||||
|
appDeployMessagePublisher.send(appCreationPayload, applicationId);
|
||||||
|
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sends the metric model message as done by the UI during deployment.
|
||||||
|
* @param metricModelPayload
|
||||||
|
* @param applicationId
|
||||||
|
*/
|
||||||
|
public void sendMetricModelMessage(Map<String,Object> metricModelPayload,String applicationId)
|
||||||
|
{
|
||||||
|
metricModelPublisher.send(metricModelPayload,applicationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
package eut.nebulouscloud.tests;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
public class SendPayloadToMQTTProcessorApp {
|
||||||
|
static Logger LOGGER = LoggerFactory.getLogger(SendPayloadToMQTTProcessorApp.class);
|
||||||
|
static protected ObjectMapper om = new ObjectMapper();
|
||||||
|
static int DELAY_SECONDS = 3;
|
||||||
|
static MQTTProcessorAppMessageBrokerInterface appBroker;
|
||||||
|
static String mqttBroker = "broker.emqx.io";
|
||||||
|
static String mqttPort = "1883";
|
||||||
|
static String mqttTopicPrefix = "atest";
|
||||||
|
static String mqttAppInputTopic = mqttTopicPrefix + "/input";
|
||||||
|
static String mqttAppOutputTopic = mqttTopicPrefix + "/output";
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception
|
||||||
|
{
|
||||||
|
//String applicationId = "application=1431290905automated-testing-mqtt-app-1715257889393";
|
||||||
|
String applicationId = "1549030905automated-testing-mqtt-app-1715262543304";
|
||||||
|
LOGGER.info(String.format("Begin MQTT Processor APP deployment. applicationId is %s", applicationId));
|
||||||
|
appBroker = new MQTTProcessorAppMessageBrokerInterface("tcp://" + mqttBroker + ":" + mqttPort, mqttAppOutputTopic);
|
||||||
|
while(true)
|
||||||
|
{
|
||||||
|
if(sendPayloadAndWaitAnswer(20) == false)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}else
|
||||||
|
{
|
||||||
|
LOGGER.info("App responded");
|
||||||
|
}
|
||||||
|
Thread.sleep(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean sendPayloadAndWaitAnswer(int timeoutSeconds) throws Exception {
|
||||||
|
long timeout = new Date().getTime() + (timeoutSeconds * 1000);
|
||||||
|
int retriesLeft = 2;
|
||||||
|
do {
|
||||||
|
/**
|
||||||
|
* Build a request to be sent to the application input topic.
|
||||||
|
*/
|
||||||
|
Map<String, Object> inferenceRequest = new HashMap<String, Object>();
|
||||||
|
inferenceRequest.put("timestamp", new SimpleDateFormat("YYYY-MM-dd HH:mm:ssZ").format(new Date()));
|
||||||
|
inferenceRequest.put("job_timestamp", inferenceRequest.get("timestamp"));
|
||||||
|
inferenceRequest.put("inference_duration", 5);
|
||||||
|
String jobId = UUID.randomUUID().toString();
|
||||||
|
inferenceRequest.put("job_id", jobId);
|
||||||
|
String payload = om.writeValueAsString(inferenceRequest);
|
||||||
|
// Send the request
|
||||||
|
appBroker.publish(mqttAppInputTopic, payload);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the application sends a message to the response channel with
|
||||||
|
* apropriate structure (check it is a JSON and has the job_id value). If found,
|
||||||
|
* we can consider the app is running
|
||||||
|
*/
|
||||||
|
if (appBroker.findFirst(m -> {
|
||||||
|
return m.jsonPayload() != null && jobId.equals(m.jsonPayload().get("job_id"));
|
||||||
|
}, 3).isPresent()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there is a message with the content "Error in STOMP connection" it means
|
||||||
|
* that the APP is not able to publish metrics to EMS using STOMP. In this
|
||||||
|
* situation, retry at most two times.
|
||||||
|
*/
|
||||||
|
if (appBroker.findFirst(m -> "Error in STOMP connection".equals(m.payload), 3).isPresent()) {
|
||||||
|
retriesLeft--;
|
||||||
|
LOGGER.error("APP is reporting initialization error. Retries left:" + retriesLeft);
|
||||||
|
appBroker.clearMessageCache();
|
||||||
|
if (retriesLeft == 0)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} while (new Date().getTime() < timeout);
|
||||||
|
LOGGER.error("Timeout waiting for a message");
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"title": "{{APP_ID}}",
|
||||||
|
"uuid": "{{APP_ID}}",
|
||||||
|
"status": "deploying",
|
||||||
|
"content": "apiVersion: \"core.oam.dev/v1beta1\"\r\nkind: \"Application\"\r\nmetadata:\r\n name: \"{{APP_ID}}\"\r\nspec:\r\n components:\r\n - name: \"dummy-app-worker\"\r\n type: \"webservice\"\r\n properties:\r\n image: \"docker.io/rsprat/mytestrepo:v1\"\r\n cpu: \"2.0\"\r\n memory: \"2048Mi\"\r\n imagePullPolicy: \"Always\"\r\n cmd:\r\n - \"python\"\r\n - \"-u\"\r\n - \"worker.py\"\r\n env:\r\n - name: \"mqtt_ip\"\r\n value: \"{{APP_MQTT_BROKER_SERVER}}\"\r\n - name: \"mqtt_port\"\r\n value: \"{{APP_MQTT_BROKER_PORT}}\"\r\n - name: \"mqtt_subscribe_topic\"\r\n value: \"{{APP_MQTT_INPUT_TOPIC}}\"\r\n - name: \"mqtt_publish_topic\"\r\n value: \"{{APP_MQTT_OUTPUT_TOPIC}}\"\r\n - name: \"report_metrics_to_ems\"\r\n value: \"{{REPORT_METRICS_TO_EMS}}\"\r\n - name: \"nebulous_ems_ip\"\r\n valueFrom:\r\n fieldRef:\r\n fieldPath: status.hostIP\r\n - name: \"nebulous_ems_port\"\r\n value: \"61610\"\r\n - name: \"nebulous_ems_user\"\r\n value: \"aaa\"\r\n - name: \"nebulous_ems_password\"\r\n value: \"111\"\r\n - name: \"nebulous_ems_metrics_topic\"\r\n value: \"/topic/RawProcessingLatency_SENSOR\"\r\n traits:\r\n - type: \"scaler\"\r\n properties:\r\n replicas: 1\r\n\r\n\r\n policies:\r\n - name: \"target-default\"\r\n type: \"topology\"\r\n properties:\r\n namespace: \"default\"\r\n workflow:\r\n steps:\r\n - name: \"deploy2default\"\r\n type: \"deploy\"\r\n properties:\r\n policies:\r\n - \"target-default\"",
|
||||||
|
"variables": [
|
||||||
|
{
|
||||||
|
"key": "spec_components_0_traits_0_properties_replicas",
|
||||||
|
"path": "/spec/components/0/traits/0/properties/replicas",
|
||||||
|
"type": "float",
|
||||||
|
"meaning": "replicas",
|
||||||
|
"value": {
|
||||||
|
"lower_bound": 1,
|
||||||
|
"higher_bound": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"environmentVariables": [],
|
||||||
|
"resources": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"templates": [],
|
||||||
|
"parameters": [],
|
||||||
|
"metrics": [
|
||||||
|
{
|
||||||
|
"type": "raw",
|
||||||
|
"name": "RawProcessingLatency",
|
||||||
|
"level": "global",
|
||||||
|
"components": [],
|
||||||
|
"sensor": "job_process_time_instance",
|
||||||
|
"config": [],
|
||||||
|
"isWindowOutputRaw": true,
|
||||||
|
"outputRaw": {
|
||||||
|
"type": "all",
|
||||||
|
"interval": 30,
|
||||||
|
"unit": "sec"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "composite",
|
||||||
|
"level": "global",
|
||||||
|
"components": [],
|
||||||
|
"name": "MeanJobProcessingLatency",
|
||||||
|
"template": "",
|
||||||
|
"formula": "mean(RawProcessingLatency)",
|
||||||
|
"isWindowInput": true,
|
||||||
|
"input": {
|
||||||
|
"type": "sliding",
|
||||||
|
"interval": 30,
|
||||||
|
"unit": "sec"
|
||||||
|
},
|
||||||
|
"isWindowOutput": true,
|
||||||
|
"output": {
|
||||||
|
"type": "all",
|
||||||
|
"interval": 30,
|
||||||
|
"unit": "sec"
|
||||||
|
},
|
||||||
|
"arguments": [
|
||||||
|
"RawProcessingLatency"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sloViolations": {
|
||||||
|
"nodeKey": "5ce4273e-5ac3-478b-b460-075b053fb994",
|
||||||
|
"isComposite": true,
|
||||||
|
"condition": "AND",
|
||||||
|
"not": false,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"nodeKey": "982c13a8-bbae-4574-b2be-eca15b865563",
|
||||||
|
"isComposite": false,
|
||||||
|
"metricName": "MeanJobProcessingLatency",
|
||||||
|
"operator": ">",
|
||||||
|
"value": 50
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"utilityFunctions": [
|
||||||
|
{
|
||||||
|
"name": "f",
|
||||||
|
"type": "minimize",
|
||||||
|
"expression": {
|
||||||
|
"formula": "(dummy_app_worker_MeanJobProcessingLatency*currentReplicas)/spec_components_0_traits_0_properties_replicas",
|
||||||
|
"variables": [
|
||||||
|
{
|
||||||
|
"name": "dummy_app_worker_MeanJobProcessingLatency",
|
||||||
|
"value": "MeanJobProcessingLatency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "currentReplicas",
|
||||||
|
"value": "currentReplicas"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "spec_components_0_traits_0_properties_replicas",
|
||||||
|
"value": "spec_components_0_traits_0_properties_replicas"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "currentReplicas",
|
||||||
|
"type": "constant",
|
||||||
|
"expression": {
|
||||||
|
"formula": "a",
|
||||||
|
"variables": [
|
||||||
|
{
|
||||||
|
"name": "a",
|
||||||
|
"value": "spec_components_0_traits_0_properties_replicas"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"_create": true,
|
||||||
|
"_delete": true
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
{"application":"{{APP_ID}}","yaml":{"apiVersion":"nebulous/v1","kind":"MetricModel","metadata":{"name":"{{APP_ID}}","labels":{"app":"{{APP_ID}}"}},"templates":[],"spec":{"components":[{"name":"spec-comp","metrics":[]}],"scopes":[{"name":"app-wide-scope","components":[],"metrics":[{"name":"RawProcessingLatency","type":"raw","sensor":{"type":"job_process_time_instance","config":{}},"output":"all 30 sec"},{"name":"MeanJobProcessingLatency","type":"composite","template":"","formula":"mean(RawProcessingLatency)","window":{"type":"sliding","size":"30 sec"},"output":"all 30 sec"}],"requirements":[{"name":"Combined_SLO","type":"slo","constraint":"(MeanJobProcessingLatency > 50)"}]}]}}}
|
Loading…
x
Reference in New Issue
Block a user