diff --git a/src/components/experiment-list/experiment-list-element.css b/src/components/experiment-list/experiment-list-element.css index 7383c3a3efe8d0d7d59ffe9514a8a9048f950a9e..8ce8b3aa4cfec6c89bd6a5cf73575b243461ab9f 100644 --- a/src/components/experiment-list/experiment-list-element.css +++ b/src/components/experiment-list/experiment-list-element.css @@ -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 diff --git a/src/components/experiment-list/experiment-list-element.js b/src/components/experiment-list/experiment-list-element.js index 1734b1caae2ca8b03c2cd751d30a2bdf5624f4af..907d5af87ac91816090a1dea720b4b524767fd06 100644 --- a/src/components/experiment-list/experiment-list-element.js +++ b/src/components/experiment-list/experiment-list-element.js @@ -1,7 +1,7 @@ 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 ? diff --git a/src/components/experiment-list/simulation-details.js b/src/components/experiment-list/simulation-details.js index 10b3771432030f8f50f8863e0263c3f0028e196b..98fbd4a6ca85151cdd2c565c090d33d2ac742ce7 100644 --- a/src/components/experiment-list/simulation-details.js +++ b/src/components/experiment-list/simulation-details.js @@ -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 diff --git a/src/components/experiment-overview/experiment-overview.js b/src/components/experiment-overview/experiment-overview.js index e622a67ac0075873e81b44e61f9787c7540012ad..6b67ad2b22f13efd8520d3744fc8fb24c571d2ea 100644 --- a/src/components/experiment-overview/experiment-overview.js +++ b/src/components/experiment-overview/experiment-overview.js @@ -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'; diff --git a/src/services/experiments/execution/experiment-execution-service.js b/src/services/experiments/execution/experiment-execution-service.js index c12d93c2dca2077ea76b04a83fffca22e331b1eb..2ec73f548d385d9f06dd5d5de9476bb08337fd7e 100644 --- a/src/services/experiments/execution/experiment-execution-service.js +++ b/src/services/experiments/execution/experiment-execution-service.js @@ -1,7 +1,8 @@ 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(); diff --git a/src/services/experiments/execution/server-resources-service.js b/src/services/experiments/execution/server-resources-service.js new file mode 100644 index 0000000000000000000000000000000000000000..7c03b2a3f0957358da0f9674e3edd1c8e594735b --- /dev/null +++ b/src/services/experiments/execution/server-resources-service.js @@ -0,0 +1,139 @@ +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; diff --git a/src/services/experiments/execution/experiment-server-service.js b/src/services/experiments/execution/simulation-service.js similarity index 57% rename from src/services/experiments/execution/experiment-server-service.js rename to src/services/experiments/execution/simulation-service.js index b9f17b2a4f8fb83864d3e797837f50794939681c..6817439f662822827385f52acf8d3b9d41c13445 100644 --- a/src/services/experiments/execution/experiment-server-service.js +++ b/src/services/experiments/execution/simulation-service.js @@ -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; diff --git a/src/services/experiments/storage/experiment-storage-service.js b/src/services/experiments/storage/experiment-storage-service.js index a63038cff7c8a97e4a7812ca8531bfb4d731a116..23284d15c77176ade86f5736c963883355785249 100644 --- a/src/services/experiments/storage/experiment-storage-service.js +++ b/src/services/experiments/storage/experiment-storage-service.js @@ -12,7 +12,7 @@ const storageImportExperiment = `${config.api.proxy.url}${endpoints.proxy.storag 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 @@ -50,7 +50,7 @@ class ExperimentStorageService extends HttpService { () => { this.getExperiments(true); }, - POLL_INTERVAL_EXPERIMENTS + INTERVAL_POLL_EXPERIMENTS ); } @@ -75,7 +75,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); } @@ -97,6 +96,9 @@ class ExperimentStorageService extends HttpService { return image; } + /** + * Sort the local list of experiments alphabetically. + */ sortExperiments() { this.experiments = this.experiments.sort( (a, b) => { @@ -113,6 +115,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) {