Skip to content
Snippets Groups Projects
Commit a2bc2f26 authored by Sandro Weber's avatar Sandro Weber
Browse files

service structure

parent 365751e7
No related branches found
No related tags found
No related merge requests found
......@@ -113,4 +113,15 @@
.server-status-restricted {
background-color: #f0ad4e;
}
.h4 {
font-size: 1.2em;
font-weight: bold;
padding-bottom: 10px;
}
.exp-title-sim-info {
padding-left: 20px;
font-style: italic;
}
\ No newline at end of file
import React from 'react';
import timeDDHHMMSS from '../../utility/time-filter.js';
import ExperimentStorageService from '../../services/experiments/storage/experiment-storage-service.js';
import ExperimentServerService from '../../services/experiments/execution/experiment-server-service.js';
import ExperimentServerService from '../../services/experiments/execution/server-resources-service.js';
import ExperimentExecutionService from '../../services/experiments/execution/experiment-execution-service.js';
import SimulationDetails from './simulation-details';
......@@ -131,7 +131,11 @@ export default class ExperimentListElement extends React.Component {
<div className='h4'>
{exp.configuration.name}
</div>
<br />
{exp.joinableServers.length > 0 ?
<div className='exp-title-sim-info'>
({exp.joinableServers.length} simulation{exp.joinableServers.length > 1 ? 's' : ''} running)
</div>
: null}
</div>
<div>
{!this.state.selected && exp.configuration.description.length > SHORT_DESCRIPTION_LENGTH ?
......
......@@ -73,18 +73,17 @@ export default class SimulationDetails extends React.Component {
<div>{simulation.runningSimulation.state}</div>
<div>
{/* Join button enabled provided simulation state is consistent */}
<button analytics-on analytics-event="Join" analytics-category="Experiment"
<button /*analytics-on analytics-event="Join" analytics-category="Experiment"
ng-click="(simulation.runningSimulation.state === STATE.CREATED) ||
simulation.stopping || joinExperiment(simulation, exp);"
simulation.stopping || joinExperiment(simulation, exp);"*/
type="button" className="btn btn-default"
disabled={this.isJoinDisabled(simulation)}>
Join »
</button>
{/* Stop button enabled provided simulation state is consistent */}
<button analytics-on analytics-event="Stop" analytics-category="Experiment"
<button /*analytics-on analytics-event="Stop" analytics-category="Experiment"*/
onClick={() => ExperimentExecutionService.instance.stopExperiment(simulation)}
type="button" className="btn btn-default"
ng-if="canStopSimulation(simulation)"
disabled={this.isStopDisabled(simulation)}
title={this.state.titleButtonStop}>
<i className="fa fa-spinner fa-spin" ng-if="simulation.stopping"></i> Stop
......
......@@ -3,7 +3,7 @@ import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import 'react-tabs/style/react-tabs.css';
import ExperimentStorageService from '../../services/experiments/storage/experiment-storage-service.js';
import ExperimentServerService from '../../services/experiments/execution/experiment-server-service.js';
import ExperimentServerService from '../../services/experiments/execution/server-resources-service.js';
import ExperimentExecutionService from '../../services/experiments/execution/experiment-execution-service.js';
import ExperimentList from '../experiment-list/experiment-list.js';
......
import _ from 'lodash';
import NrpAnalyticsService from '../../nrp-analytics-service.js';
import ExperimentServerService from './experiment-server-service.js';
import ServerResourcesService from './server-resources-service.js';
import SimulationService from './simulation-service.js';
import { HttpService } from '../../http-service.js';
import { EXPERIMENT_STATE } from '../experiment-constants.js';
......@@ -53,13 +54,13 @@ class ExperimentExecutionService extends HttpService {
let fatalErrorOccurred = false;
let serversToTry = experiment.devServer
? [experiment.devServer]
: ExperimentServerService.instance.getServerAvailability(true).map(s => s.id);
: ServerResourcesService.instance.getServerAvailability(true).map(s => s.id);
let brainProcesses = launchSingleMode ? 1 : experiment.configuration.brainProcesses;
//TODO: placeholder, register actual progress callback later
let progressCallback = (msg) => {
//console.info(msg);
console.info(msg);
};
let launchOnNextServer = async () => {
......@@ -70,7 +71,7 @@ class ExperimentExecutionService extends HttpService {
}
let server = nextServer[0];
let serverConfig = await ExperimentServerService.instance.getServerConfig(server);
let serverConfig = await ServerResourcesService.instance.getServerConfig(server);
return await this.launchExperimentOnServer(
experiment.id,
......@@ -148,14 +149,14 @@ class ExperimentExecutionService extends HttpService {
progressCallback({ main: 'Initialize Simulation...' });
// register for messages during initialization
ExperimentServerService.instance.registerForRosStatusInformation(
SimulationService.instance.registerForRosStatusInformation(
serverConfiguration.rosbridge.websocket,
progressCallback
);
ExperimentServerService.instance.simulationReady(serverURL, simInitData.creationUniqueID)
SimulationService.instance.simulationReady(serverURL, simInitData.creationUniqueID)
.then((simulation) => {
ExperimentServerService.instance.initConfigFiles(serverURL, simulation.simulationID)
SimulationService.instance.initConfigFiles(serverURL, simulation.simulationID)
.then(() => {
let simulationURL = 'esv-private/experiment-view/' + server + '/' + experimentID + '/' +
privateExperiment + '/' + simulation.simulationID;
......@@ -183,7 +184,7 @@ class ExperimentExecutionService extends HttpService {
simulationID: simulation.runningSimulation.experimentID
});*/
ExperimentServerService.instance
ServerResourcesService.instance
.getServerConfig(simulation.server)
.then((serverConfig) => {
let serverURL = serverConfig.gzweb['nrp-services'];
......@@ -191,15 +192,15 @@ class ExperimentExecutionService extends HttpService {
function updateSimulationState(state) {
/*eslint-disable camelcase*/
return ExperimentServerService.instance.updateSimulationState(
return SimulationService.instance.updateState(
serverURL,
simulationID,
{ state: state }
);
}
return ExperimentServerService.instance
.getSimulationState(serverURL, simulationID)
return SimulationService.instance
.getState(serverURL, simulationID)
.then((data) => {
if (!data || !data.state) {
return Promise.reject();
......
import _ from 'lodash';
import { Subject, timer } from 'rxjs';
import { switchMap, filter, map, multicast } from 'rxjs/operators';
import ErrorHandlerService from '../../error-handler-service.js';
import { HttpService } from '../../http-service.js';
import endpoints from '../../proxy/data/endpoints.json';
import config from '../../../config.json';
const proxyServerURL = `${config.api.proxy.url}${endpoints.proxy.server.url}`;
const slurmMonitorURL = `${config.api.slurmmonitor.url}/api/v1/partitions/interactive`;
const availableServersURL = `${config.api.proxy.url}${endpoints.proxy.availableServers.url}`;
let _instance = null;
const SINGLETON_ENFORCER = Symbol();
const INTERVAL_POLL_SLURM_MONITOR = 5000;
const INTERVAL_POLL_SERVER_AVAILABILITY = 3000;
let clusterAvailability = { free: 'N/A', total: 'N/A' };
/**
* Service handling server resources for simulating experiments.
*/
class ServerResourcesService extends HttpService {
constructor(enforcer) {
super();
if (enforcer !== SINGLETON_ENFORCER) {
throw new Error('Use ' + this.constructor.name + '.instance');
}
this.availableServers = [];
this.startUpdates();
window.onbeforeunload = () => {
this.stopUpdates();
};
}
static get instance() {
if (_instance == null) {
_instance = new ServerResourcesService(SINGLETON_ENFORCER);
}
return _instance;
}
/**
* Start polling updates.
*/
startUpdates() {
this.clusterAvailabilityObservable = this._createSlurmMonitorObservable();
this.clusterAvailabilitySubscription = this.clusterAvailabilityObservable.subscribe(
availability => (clusterAvailability = availability)
);
this.getServerAvailability(true);
this.intervalGetServerAvailability = setInterval(
() => {
this.getServerAvailability(true);
},
INTERVAL_POLL_SERVER_AVAILABILITY
);
}
/**
* Stop polling updates.
*/
stopUpdates() {
this.clusterAvailabilitySubscription && this.clusterAvailabilitySubscription.unsubscribe();
this.intervalGetServerAvailability && clearInterval(this.intervalGetServerAvailability);
}
/**
* Get available cluster server info.
* @returns {object} cluster availability info
*/
getClusterAvailability() {
return clusterAvailability;
}
/**
* Return a list of available servers for starting simulations.
* @param {boolean} forceUpdate force an update
* @returns {Array} A list of available servers.
*/
getServerAvailability(forceUpdate = false) {
if (!this.availableServers || forceUpdate) {
let update = async () => {
let response = await this.httpRequestGET(availableServersURL);
this.availableServers = await response.json();
};
update();
this.emit(ServerResourcesService.EVENTS.UPDATE_SERVER_AVAILABILITY, this.availableServers);
}
return this.availableServers;
}
/**
* Get the server config for a given server ID.
* @param {string} serverID - ID of the server
* @returns {object} The server configuration
*/
getServerConfig(serverID) {
return this.httpRequestGET(proxyServerURL + '/' + serverID)
.then(async (response) => {
return await response.json();
})
.catch(/*serverError.displayHTTPError*/ErrorHandlerService.instance.displayServerHTTPError);
}
_createSlurmMonitorObservable() {
return timer(0, INTERVAL_POLL_SLURM_MONITOR)
.pipe(switchMap(() => {
try {
return this.httpRequestGET(slurmMonitorURL);
}
catch (error) {
_.once(error => {
if (error.status === -1) {
error = Object.assign(error, {
data: 'Could not probe vizualization cluster'
});
}
ErrorHandlerService.instance.displayServerHTTPError(error);
});
}
}))
.pipe(filter(e => e))
.pipe(map(({ free, nodes }) => ({ free, total: nodes[3] })))
.pipe(multicast(new Subject())).refCount();
}
}
ServerResourcesService.EVENTS = Object.freeze({
UPDATE_SERVER_AVAILABILITY: 'UPDATE_SERVER_AVAILABILITY'
});
export default ServerResourcesService;
......@@ -7,132 +7,33 @@ import RoslibService from '../../roslib-service.js';
import { HttpService } from '../../http-service.js';
import { EXPERIMENT_STATE } from '../experiment-constants.js';
import endpoints from '../../proxy/data/endpoints.json';
import config from '../../../config.json';
const proxyServerURL = `${config.api.proxy.url}${endpoints.proxy.server.url}`;
const slurmMonitorURL = `${config.api.slurmmonitor.url}/api/v1/partitions/interactive`;
const availableServersURL = `${config.api.proxy.url}${endpoints.proxy.availableServers.url}`;
let _instance = null;
const SINGLETON_ENFORCER = Symbol();
let rosConnections = new Map();
const INTERVAL_POLL__SLURM_MONITOR = 5000;
const INTERVAL_POLL_SERVER_AVAILABILITY = 3000;
const INTERVAL_CHECK_SIMULATION_READY = 1000;
let clusterAvailability = { free: 'N/A', total: 'N/A' };
/**
* Service handling server resources for simulating experiments.
* Service handling state and info of running simulations.
*/
class ExperimentServerService extends HttpService {
class SimulationService extends HttpService {
constructor(enforcer) {
super();
if (enforcer !== SINGLETON_ENFORCER) {
throw new Error('Use ' + this.constructor.name + '.instance');
}
//TODO: a bit too much code for a constructor, move into its own function
this.clusterAvailabilityObservable = timer(0, INTERVAL_POLL__SLURM_MONITOR)
.pipe(switchMap(() => {
try {
return this.httpRequestGET(slurmMonitorURL);
}
catch (error) {
_.once(error => {
if (error.status === -1) {
error = Object.assign(error, {
data: 'Could not probe vizualization cluster'
});
}
ErrorHandlerService.instance.displayServerHTTPError(error);
});
}
}))
.pipe(filter(e => e))
.pipe(map(({ free, nodes }) => ({ free, total: nodes[3] })))
.pipe(multicast(new Subject())).refCount();
this.availableServers = [];
this.startUpdates();
window.onbeforeunload = () => {
this.stopUpdates();
};
}
static get instance() {
if (_instance == null) {
_instance = new ExperimentServerService(SINGLETON_ENFORCER);
_instance = new SimulationService(SINGLETON_ENFORCER);
}
return _instance;
}
/**
* Start polling updates.
*/
startUpdates() {
this.clusterAvailabilitySubscription = this.clusterAvailabilityObservable.subscribe(
availability => (clusterAvailability = availability)
);
this.getServerAvailability(true);
this.intervalGetServerAvailability = setInterval(
() => {
this.getServerAvailability(true);
},
INTERVAL_POLL_SERVER_AVAILABILITY
);
}
/**
* Stop polling updates.
*/
stopUpdates() {
this.clusterAvailabilitySubscription && this.clusterAvailabilitySubscription.unsubscribe();
this.intervalGetServerAvailability && clearInterval(this.intervalGetServerAvailability);
}
/**
* Get available cluster server info.
* @returns {object} cluster availability info
*/
getClusterAvailability() {
return clusterAvailability;
}
/**
* Return a list of available servers for starting simulations.
* @param {boolean} forceUpdate force an update
* @returns {Array} A list of available servers.
*/
getServerAvailability(forceUpdate = false) {
if (!this.availableServers || forceUpdate) {
let update = async () => {
let response = await this.httpRequestGET(availableServersURL);
this.availableServers = await response.json();
};
update();
this.emit(ExperimentServerService.EVENTS.UPDATE_SERVER_AVAILABILITY, this.availableServers);
}
return this.availableServers;
}
/**
* Get the server config for a given server ID.
* @param {string} serverID - ID of the server
* @returns {object} The server configuration
*/
getServerConfig(serverID) {
return this.httpRequestGET(proxyServerURL + '/' + serverID)
.then(async (response) => {
return await response.json();
})
.catch(/*serverError.displayHTTPError*/ErrorHandlerService.instance.displayServerHTTPError);
}
/**
* Initialize config files on a server for a simulation.
* @param {string} serverBaseUrl - URL of the server
......@@ -239,8 +140,13 @@ class ExperimentServerService extends HttpService {
});
};
//TODO: maybe move to separate simulation-status-service
async getSimulationState(serverURL, simulationID) {
/**
* Get the state the simulation is currently in.
* @param {string} serverURL URL of the server the simulation is running on
* @param {number} simulationID ID of the simulation
* @returns {object} The simulation state
*/
async getState(serverURL, simulationID) {
let url = serverURL + '/simulation/' + simulationID + '/state';
try {
let response = await (await this.httpRequestGET(url)).json();
......@@ -251,7 +157,13 @@ class ExperimentServerService extends HttpService {
}
}
async updateSimulationState(serverURL, simulationID, state) {
/**
* Set the state for a simulation.
* @param {string} serverURL URL of the server the simulation is running on
* @param {number} simulationID ID of the simulation
* @param {EXPERIMENT_STATE} state state to set for the simulation
*/
async updateState(serverURL, simulationID, state) {
let url = serverURL + '/simulation/' + simulationID + '/state';
try {
let response = await this.httpRequestPUT(url, JSON.stringify(state));
......@@ -263,8 +175,4 @@ class ExperimentServerService extends HttpService {
}
}
ExperimentServerService.EVENTS = Object.freeze({
UPDATE_SERVER_AVAILABILITY: 'UPDATE_SERVER_AVAILABILITY'
});
export default ExperimentServerService;
export default SimulationService;
/**
* @jest-environment node
* @jest-environment jsdom
*/
import '@testing-library/jest-dom';
import 'jest-fetch-mock';
......@@ -19,7 +19,6 @@ test('fetches the list of experiments', async () => {
.toHaveBeenCalledWith(experimentsUrl, ExperimentStorageService.instance.options);
expect(experiments[0].name).toBe('braitenberg_husky_holodeck_1_0_0');
expect(experiments[1].configuration.maturity).toBe('production');
expect(experiments[1].availableServers[0].internalIp).toBe('http://localhost:8080');
});
test('makes sure that invoking the constructor fails with the right message', () => {
......
......@@ -7,7 +7,7 @@ const storageExperimentsURL = `${config.api.proxy.url}${endpoints.proxy.storage.
let _instance = null;
const SINGLETON_ENFORCER = Symbol();
const POLL_INTERVAL_EXPERIMENTS = 3000;
const INTERVAL_POLL_EXPERIMENTS = 3000;
/**
* Service that fetches the template experiments list from the proxy given
......@@ -43,7 +43,7 @@ class ExperimentStorageService extends HttpService {
() => {
this.getExperiments(true);
},
POLL_INTERVAL_EXPERIMENTS
INTERVAL_POLL_EXPERIMENTS
);
}
......@@ -68,7 +68,6 @@ class ExperimentStorageService extends HttpService {
this.experiments = await response.json();
this.sortExperiments();
await this.fillExperimentDetails();
console.info('experiment lsit update');
this.emit(ExperimentStorageService.EVENTS.UPDATE_EXPERIMENTS, this.experiments);
}
......@@ -90,6 +89,9 @@ class ExperimentStorageService extends HttpService {
return image;
}
/**
* Sort the local list of experiments alphabetically.
*/
sortExperiments() {
this.experiments = this.experiments.sort(
(a, b) => {
......@@ -106,6 +108,9 @@ class ExperimentStorageService extends HttpService {
);
}
/**
* Fill in some details for the local experiment list that might be missing.
*/
async fillExperimentDetails() {
this.experiments.forEach(exp => {
if (!exp.configuration.brainProcesses && exp.configuration.bibiConfSrc) {
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment