Skip to content
Snippets Groups Projects
Commit 1d276134 authored by Sandro Weber's avatar Sandro Weber Committed by Antoine Detailleur
Browse files

lots more test coverage

parent 83db6277
No related branches found
No related tags found
No related merge requests found
Showing
with 391 additions and 135 deletions
...@@ -12,27 +12,15 @@ ...@@ -12,27 +12,15 @@
} }
.flex-container .selected { .flex-container .selected {
background: linear-gradient( background: linear-gradient( to right, rgba(255, 255, 255, 0.8) 0%, rgba(245, 245, 245, 0.8) 100%);
to right,
rgba(255, 255, 255, 0.8) 0%,
rgba(245, 245, 245, 0.8) 100%
);
} }
.flex-container.hover-wizard { .flex-container.hover-wizard {
background: linear-gradient( background: linear-gradient( to right, rgba(255, 255, 255, 0.8) 0%, rgba(230, 239, 255, 0.8) 100%);
to right,
rgba(255, 255, 255, 0.8) 0%,
rgba(230, 239, 255, 0.8) 100%
);
} }
.flex-container.selected-wizard { .flex-container.selected-wizard {
background: linear-gradient( background: linear-gradient( to right, rgba(255, 255, 255, 0.8) 0%, rgba(230, 239, 255, 0.8) 100%);
to right,
rgba(255, 255, 255, 0.8) 0%,
rgba(230, 239, 255, 0.8) 100%
);
} }
.flex-container.left-right { .flex-container.left-right {
...@@ -69,12 +57,13 @@ ...@@ -69,12 +57,13 @@
width: 359px; width: 359px;
} }
.list-entry-left > img { .list-entry-left>img {
width: 100%; width: 100%;
object-fit: contain; object-fit: contain;
object-position: top; object-position: top;
height: 100%; height: 100%;
} }
.list-entry-middle { .list-entry-middle {
flex: 1; flex: 1;
} }
...@@ -124,4 +113,9 @@ ...@@ -124,4 +113,9 @@
.exp-title-sim-info { .exp-title-sim-info {
padding-left: 20px; padding-left: 20px;
font-style: italic; font-style: italic;
}
.entity-thumbnail {
max-width: 230px;
max-height: 160px;
} }
\ No newline at end of file
...@@ -53,10 +53,8 @@ export default class ExperimentListElement extends React.Component { ...@@ -53,10 +53,8 @@ export default class ExperimentListElement extends React.Component {
} }
getAvailabilityInfo() { getAvailabilityInfo() {
const clusterAvailability = ExperimentServerService.instance.getClusterAvailability();
let status; let status;
if (clusterAvailability && clusterAvailability.free > CLUSTER_THRESHOLDS.AVAILABLE) { if (this.props.availableServers && this.props.availableServers.length > CLUSTER_THRESHOLDS.AVAILABLE) {
status = 'Available'; status = 'Available';
} }
else if (!this.props.availableServers || this.props.availableServers.length === 0) { else if (!this.props.availableServers || this.props.availableServers.length === 0) {
...@@ -66,17 +64,14 @@ export default class ExperimentListElement extends React.Component { ...@@ -66,17 +64,14 @@ export default class ExperimentListElement extends React.Component {
status = 'Restricted'; status = 'Restricted';
} }
let cluster = `Cluster availability: ${clusterAvailability.free} / ${clusterAvailability.total}`;
let backends = `Backends: ${this.props.availableServers.length}`; let backends = `Backends: ${this.props.availableServers.length}`;
return `${status}\n${cluster}\n${backends}`; return `${status}\n${backends}`;
} }
getServerStatusClass() { getServerStatusClass() {
const clusterAvailability = ExperimentServerService.instance.getClusterAvailability();
let status = ''; let status = '';
if (clusterAvailability && clusterAvailability.free > CLUSTER_THRESHOLDS.AVAILABLE) { if (this.props.availableServers && this.props.availableServers.length > CLUSTER_THRESHOLDS.AVAILABLE) {
status = 'server-status-available'; status = 'server-status-available';
} }
else if (!this.props.availableServers || this.props.availableServers.length === 0) { else if (!this.props.availableServers || this.props.availableServers.length === 0) {
...@@ -171,12 +166,13 @@ export default class ExperimentListElement extends React.Component { ...@@ -171,12 +166,13 @@ export default class ExperimentListElement extends React.Component {
<div className='btn-group' role='group' > <div className='btn-group' role='group' >
{this.canLaunchExperiment && {this.canLaunchExperiment &&
exp.configuration.experimentFile && exp.configuration.bibiConfSrc ? exp.configuration.experimentFile && exp.configuration.bibiConfSrc ?
<button onClick={() => { <button
ExperimentExecutionService.instance.startNewExperiment(exp, false); onClick={() => {
}} ExperimentExecutionService.instance.startNewExperiment(exp, false);
disabled={this.isLaunchDisabled()} }}
className='btn btn-default' disabled={this.isLaunchDisabled()}
title={this.launchButtonTitle} > className='btn btn-default'
title={this.launchButtonTitle} >
<i className='fa fa-plus'></i> Launch <i className='fa fa-plus'></i> Launch
</button> </button>
: null} : null}
......
...@@ -16,21 +16,18 @@ export default class UserMenu extends React.Component { ...@@ -16,21 +16,18 @@ export default class UserMenu extends React.Component {
}; };
} }
componentDidMount() { async componentDidMount() {
this._userRequest = NrpUserService.instance NrpUserService.instance.getCurrentUser().then((currentUser) => {
.getCurrentUser() if (!this.cancelGetCurrentUser) {
.then((currentUser) => { this.setState({
this._userRequest = null;
this.setState(() => ({
user: currentUser user: currentUser
})); });
}); }
});
} }
componentWillUnmount() { componentWillUnmount() {
if (this._userRequest) { this.cancelGetCurrentUser = true;
this._userRequest.cancel();
}
} }
onClickLogout() { onClickLogout() {
......
...@@ -5,6 +5,9 @@ import endpoints from '../services/proxy/data/endpoints'; ...@@ -5,6 +5,9 @@ import endpoints from '../services/proxy/data/endpoints';
import MockExperiments from './mock_experiments.json'; import MockExperiments from './mock_experiments.json';
import MockAvailableServers from './mock_available-servers.json'; import MockAvailableServers from './mock_available-servers.json';
import MockSimulationResources from './mock_simulation-resources.json'; import MockSimulationResources from './mock_simulation-resources.json';
import MockServerConfig from './mock_server-config.json';
import MockUser from './mock_user.json';
import MockSimulations from './mock_simulations.json';
import ImageAI from '../assets/images/Artificial_Intelligence_2.jpg'; import ImageAI from '../assets/images/Artificial_Intelligence_2.jpg';
...@@ -13,14 +16,10 @@ const experiments = MockExperiments; ...@@ -13,14 +16,10 @@ const experiments = MockExperiments;
export const handlers = [ export const handlers = [
rest.get(`${config.api.proxy.url}${endpoints.proxy.storage.experiments.url}`, (req, res, ctx) => { rest.get(`${config.api.proxy.url}${endpoints.proxy.storage.experiments.url}`, (req, res, ctx) => {
return res( return res(ctx.json(experiments));
ctx.json(experiments)
);
}), }),
rest.get(`${config.api.proxy.url}${endpoints.proxy.availableServers.url}`, (req, res, ctx) => { rest.get(`${config.api.proxy.url}${endpoints.proxy.availableServers.url}`, (req, res, ctx) => {
return res( return res(ctx.json(availableServers));
ctx.json(availableServers)
);
}), }),
rest.get(`${config.api.proxy.url}${endpoints.proxy.storage.url}/:experimentName/:thumbnailFilename`, rest.get(`${config.api.proxy.url}${endpoints.proxy.storage.url}/:experimentName/:thumbnailFilename`,
(req, res, ctx) => { (req, res, ctx) => {
...@@ -35,5 +34,14 @@ export const handlers = [ ...@@ -35,5 +34,14 @@ export const handlers = [
else { else {
throw new Error('Simulation resource error'); throw new Error('Simulation resource error');
} }
}),
rest.get('http://:serverIP/simulation/', (req, res, ctx) => {
return res(ctx.json(MockSimulations[0]));
}),
rest.get(`${config.api.proxy.url}${endpoints.proxy.server.url}/:serverID`, (req, res, ctx) => {
return res(ctx.json(MockServerConfig));
}),
rest.get(`${config.api.proxy.url}${endpoints.proxy.identity.me.url}`, (req, res, ctx) => {
return res(ctx.json(MockUser));
}) })
]; ];
\ No newline at end of file
...@@ -25,6 +25,6 @@ ...@@ -25,6 +25,6 @@
"websocket": "ws://1.2.3.4:8080/rosbridge" "websocket": "ws://1.2.3.4:8080/rosbridge"
}, },
"serverJobLocation": "1.2.3.4", "serverJobLocation": "1.2.3.4",
"id": "1.2.3.4:8080" "id": "1.2.3.4-port-8080"
} }
] ]
\ No newline at end of file
{
"internalIp": "http://localhost:8080",
"gzweb": {
"assets": "http://localhost:8080/assets",
"nrp-services": "http://localhost:8080",
"videoStreaming": "http://localhost:8080/webstream/",
"websocket": "ws://localhost:8080/gzbridge"
},
"rosbridge": {
"websocket": "ws://localhost:8080/rosbridge"
},
"serverJobLocation": "local",
"id": "localhost"
}
\ No newline at end of file
{
"id": "nrpuser",
"displayName": "nrpuser"
}
\ No newline at end of file
/**
* @jest-environment jsdom
*/
import '@testing-library/jest-dom';
import 'jest-fetch-mock';
import MockExperiments from '../../../../mocks/mock_experiments.json';
import MockAvailableServers from '../../../../mocks/mock_available-servers.json';
import MockServerConfig from '../../../../mocks/mock_server-config.json';
import MockSimulations from '../../../../mocks/mock_simulations.json';
import ExperimentExecutionService from '../../../../services/experiments/execution/experiment-execution-service';
import ServerResourcesService from '../../../../services/experiments/execution/server-resources-service';
import SimulationService from '../../../../services/experiments/execution/simulation-service';
//jest.setTimeout(10000);
afterEach(() => {
jest.restoreAllMocks();
});
test('makes sure that invoking the constructor fails with the right message', () => {
expect(() => {
new ExperimentExecutionService();
}).toThrow(Error);
expect(() => {
new ExperimentExecutionService();
}).toThrowError(Error('Use ExperimentExecutionService.instance'));
});
test('the service instance always refers to the same object', () => {
const instance1 = ExperimentExecutionService.instance;
const instance2 = ExperimentExecutionService.instance;
expect(instance1).toBe(instance2);
});
test('should emit an event on starting an experiment', async () => {
jest.spyOn(ExperimentExecutionService.instance, 'launchExperimentOnServer').mockImplementation(() => {
return Promise.resolve();
});
let experiment = MockExperiments[0];
let confirmStartingExperiment = (startingExperiment) => {
expect(startingExperiment).toEqual(experiment);
};
ExperimentExecutionService.instance.addListener(
ExperimentExecutionService.EVENTS.START_EXPERIMENT,
confirmStartingExperiment
);
await ExperimentExecutionService.instance.startNewExperiment(experiment);
ExperimentExecutionService.instance.removeListener(
ExperimentExecutionService.EVENTS.START_EXPERIMENT,
confirmStartingExperiment
);
});
test('should go through the list of available servers when trying to start an experiment', (done) => {
jest.spyOn(console, 'error').mockImplementation();
jest.spyOn(ServerResourcesService.instance, 'getServerConfig');
jest.spyOn(ExperimentExecutionService.instance, 'launchExperimentOnServer').mockImplementation(
// only the last server in the list will return a successful launch
(expID, isPrivate, numBrainProc, serverID) => {
if (serverID !== MockAvailableServers[MockAvailableServers.length - 1].id) {
return Promise.reject({
error: {
data: 'test rejection for launch on server ' + serverID
}
});
}
return Promise.resolve();
}
);
let experiment = MockExperiments[0];
ExperimentExecutionService.instance.startNewExperiment(experiment).then(() => {
MockAvailableServers.forEach(server => {
expect(ServerResourcesService.instance.getServerConfig).toHaveBeenCalledWith(server.id);
});
expect(console.error).toHaveBeenCalled();
done();
});
});
test('starting an experiment should abort early if a fatal error occurs', (done) => {
jest.spyOn(ExperimentExecutionService.instance, 'launchExperimentOnServer').mockImplementation(
() => {
return Promise.reject({
isFatal: true
});
}
);
let experiment = MockExperiments[0];
ExperimentExecutionService.instance.startNewExperiment(experiment).catch(error => {
expect(error).toEqual(ExperimentExecutionService.ERRORS.LAUNCH_FATAL_ERROR);
done();
});
});
test('starting an experiment should fail if no server is ready', (done) => {
jest.spyOn(ExperimentExecutionService.instance, 'launchExperimentOnServer').mockImplementation(
() => {
return Promise.reject({});
}
);
let experiment = MockExperiments[0];
ExperimentExecutionService.instance.startNewExperiment(experiment).catch(error => {
expect(error).toEqual(ExperimentExecutionService.ERRORS.LAUNCH_NO_SERVERS_LEFT);
done();
});
});
test('respects settings for specific dev server to launch and single brain process mode', async () => {
jest.spyOn(ExperimentExecutionService.instance, 'launchExperimentOnServer').mockImplementation(() => {
return Promise.resolve();
});
let mockExperiment = {
id: 'test-experiment-id',
devServer: 'test-dev-server-url'
};
await ExperimentExecutionService.instance.startNewExperiment(mockExperiment, true);
expect(ExperimentExecutionService.instance.launchExperimentOnServer).toHaveBeenCalledWith(
mockExperiment.id,
undefined,
1,
mockExperiment.devServer,
expect.any(Object),
undefined,
undefined,
undefined,
expect.any(Function)
);
});
test('can launch an experiment given a specific server + configuration', async () => {
jest.spyOn(ExperimentExecutionService.instance, 'httpRequestPOST').mockImplementation();
jest.spyOn(SimulationService.instance, 'registerForRosStatusInformation').mockImplementation();
jest.spyOn(SimulationService.instance, 'simulationReady').mockImplementation(() => {
return Promise.resolve(MockSimulations[0]);
});
jest.spyOn(SimulationService.instance, 'initConfigFiles').mockImplementation(() => {
return Promise.resolve();
});
let experimentID = 'test-experiment-id';
let privateExperiment = true;
let brainProcesses = 2;
let serverID = 'test-server-id';
let serverConfiguration = MockServerConfig;
let reservation = {};
let playbackRecording = {};
let profiler = {};
let progressCallback = jest.fn();
let callParams = [experimentID, privateExperiment, brainProcesses, serverID, serverConfiguration, reservation,
playbackRecording, profiler, progressCallback];
let result = await ExperimentExecutionService.instance.launchExperimentOnServer(...callParams);
expect(ExperimentExecutionService.instance.httpRequestPOST)
.toHaveBeenLastCalledWith(serverConfiguration.gzweb['nrp-services'] + '/simulation', expect.any(String));
expect(progressCallback).toHaveBeenCalled();
expect(result).toBe('esv-private/experiment-view/' + serverID + '/' + experimentID + '/' +
privateExperiment + '/' + MockSimulations[0].simulationID);
});
/**
* @jest-environment jsdom
*/
import '@testing-library/jest-dom';
import 'jest-fetch-mock';
import MockServerconfig from '../../../../mocks/mock_server-config.json';
import ServerResourcesService from '../../../../services/experiments/execution/server-resources-service';
import ErrorHandlerService from '../../../error-handler-service';
jest.setTimeout(10000);
let onWindowBeforeUnloadCb = undefined;
beforeEach(() => {
jest.spyOn(window, 'addEventListener').mockImplementation((event, cb) => {
if (event === 'beforeunload') {
onWindowBeforeUnloadCb = cb;
}
});
});
afterEach(() => {
jest.restoreAllMocks();
});
test('makes sure that invoking the constructor fails with the right message', () => {
expect(() => {
new ServerResourcesService();
}).toThrow(Error);
expect(() => {
new ServerResourcesService();
}).toThrowError(Error('Use ServerResourcesService.instance'));
});
test('the service instance always refers to the same object', () => {
const instance1 = ServerResourcesService.instance;
const instance2 = ServerResourcesService.instance;
expect(instance1).toBe(instance2);
});
test('does automatic poll updates for server availability', (done) => {
jest.spyOn(ServerResourcesService.instance, 'getServerAvailability');
// check that getExperiments is periodically called after poll interval
let numCallsServerAvailabilityT0 = ServerResourcesService.instance.getServerAvailability.mock.calls.length;
setTimeout(() => {
let numCallsServerAvailabilityT1 = ServerResourcesService.instance.getServerAvailability.mock.calls.length;
expect(numCallsServerAvailabilityT1 > numCallsServerAvailabilityT0).toBe(true);
// stop updates and check that no more calls occur after poll interval
ServerResourcesService.instance.stopUpdates();
setTimeout(() => {
let numCallsServerAvailabilityT2 = ServerResourcesService.instance.getServerAvailability.mock.calls.length;
expect(numCallsServerAvailabilityT2 === numCallsServerAvailabilityT1).toBe(true);
done();
}, ServerResourcesService.CONSTANTS.INTERVAL_POLL_SERVER_AVAILABILITY);
}, ServerResourcesService.CONSTANTS.INTERVAL_POLL_SERVER_AVAILABILITY);
});
test('can get a server config', async () => {
// regular call with proper json
let config = await ServerResourcesService.instance.getServerConfig('test-server-id');
expect(config).toEqual(MockServerconfig);
// rejected promise on GET
jest.spyOn(ServerResourcesService.instance, 'httpRequestGET').mockImplementation(() => {
return Promise.reject();
});
jest.spyOn(ErrorHandlerService.instance, 'displayServerHTTPError').mockImplementation();
config = await ServerResourcesService.instance.getServerConfig('test-server-id');
expect(ErrorHandlerService.instance.displayServerHTTPError).toHaveBeenCalled();
});
test('should stop polling updates when window is unloaded', async () => {
let service = ServerResourcesService.instance;
expect(onWindowBeforeUnloadCb).toBeDefined();
jest.spyOn(service, 'stopUpdates');
onWindowBeforeUnloadCb({});
expect(service.stopUpdates).toHaveBeenCalled();
});
import _ from 'lodash'; import _ from 'lodash';
import NrpAnalyticsService from '../../nrp-analytics-service.js'; //import NrpAnalyticsService from '../../nrp-analytics-service.js';
import ServerResourcesService from './server-resources-service.js'; import ServerResourcesService from './server-resources-service.js';
import SimulationService from './simulation-service.js'; import SimulationService from './simulation-service.js';
import { HttpService } from '../../http-service.js'; import { HttpService } from '../../http-service.js';
...@@ -39,22 +39,23 @@ class ExperimentExecutionService extends HttpService { ...@@ -39,22 +39,23 @@ class ExperimentExecutionService extends HttpService {
* @param {object} playbackRecording - a recording of a previous execution * @param {object} playbackRecording - a recording of a previous execution
* @param {*} profiler - a profiler option * @param {*} profiler - a profiler option
*/ */
startNewExperiment( async startNewExperiment(
experiment, experiment,
launchSingleMode, launchSingleMode,
reservation, reservation,
playbackRecording, playbackRecording,
profiler profiler
) { ) {
NrpAnalyticsService.instance.eventTrack('Start', { category: 'Experiment' }); //TODO: implement NrpAnalyticsService functionality
NrpAnalyticsService.instance.tickDurationEvent('Server-initialization'); //NrpAnalyticsService.instance.eventTrack('Start', { category: 'Experiment' });
//NrpAnalyticsService.instance.tickDurationEvent('Server-initialization');
ExperimentExecutionService.instance.emit(ExperimentExecutionService.EVENTS.START_EXPERIMENT, experiment); ExperimentExecutionService.instance.emit(ExperimentExecutionService.EVENTS.START_EXPERIMENT, experiment);
let fatalErrorOccurred = false; let fatalErrorOccurred = false;
let serversToTry = experiment.devServer let serversToTry = experiment.devServer
? [experiment.devServer] ? [experiment.devServer]
: ServerResourcesService.instance.getServerAvailability(true).map(s => s.id); : (await ServerResourcesService.instance.getServerAvailability(true)).map(s => s.id);
let brainProcesses = launchSingleMode ? 1 : experiment.configuration.brainProcesses; let brainProcesses = launchSingleMode ? 1 : experiment.configuration.brainProcesses;
...@@ -64,20 +65,23 @@ class ExperimentExecutionService extends HttpService { ...@@ -64,20 +65,23 @@ class ExperimentExecutionService extends HttpService {
}; };
let launchOnNextServer = async () => { let launchOnNextServer = async () => {
let nextServer = serversToTry.splice(0, 1); if (!serversToTry.length) {
if (fatalErrorOccurred || !nextServer.length) { //TODO: GUI feedback
//no more servers to retry, we have failed to start experiment return Promise.reject(ExperimentExecutionService.ERRORS.LAUNCH_NO_SERVERS_LEFT);
return Promise.reject(fatalErrorOccurred); }
if (fatalErrorOccurred) {
//TODO: GUI feedback
return Promise.reject(ExperimentExecutionService.ERRORS.LAUNCH_FATAL_ERROR);
} }
let server = nextServer[0]; let serverID = serversToTry.splice(0, 1)[0];
let serverConfig = await ServerResourcesService.instance.getServerConfig(server); let serverConfig = await ServerResourcesService.instance.getServerConfig(serverID);
return await this.launchExperimentOnServer( return await this.launchExperimentOnServer(
experiment.id, experiment.id,
experiment.private, experiment.private,
brainProcesses, brainProcesses,
server, serverID,
serverConfig, serverConfig,
reservation, reservation,
playbackRecording, playbackRecording,
...@@ -85,6 +89,7 @@ class ExperimentExecutionService extends HttpService { ...@@ -85,6 +89,7 @@ class ExperimentExecutionService extends HttpService {
progressCallback progressCallback
).catch((failure) => { ).catch((failure) => {
if (failure.error && failure.error.data) { if (failure.error && failure.error.data) {
//TODO: proper ErrorHandlerService callback
console.error('Failed to start simulation: ' + JSON.stringify(failure.error.data)); console.error('Failed to start simulation: ' + JSON.stringify(failure.error.data));
} }
fatalErrorOccurred = fatalErrorOccurred || failure.isFatal; fatalErrorOccurred = fatalErrorOccurred || failure.isFatal;
...@@ -101,7 +106,7 @@ class ExperimentExecutionService extends HttpService { ...@@ -101,7 +106,7 @@ class ExperimentExecutionService extends HttpService {
* @param {string} experimentID - ID of the experiment to launch * @param {string} experimentID - ID of the experiment to launch
* @param {boolean} privateExperiment - whether the experiment is private or not * @param {boolean} privateExperiment - whether the experiment is private or not
* @param {number} brainProcesses - number of brain processes to start with * @param {number} brainProcesses - number of brain processes to start with
* @param {string} server - server ID * @param {string} serverID - server ID
* @param {object} serverConfiguration - configuration of server * @param {object} serverConfiguration - configuration of server
* @param {object} reservation - server reservation * @param {object} reservation - server reservation
* @param {object} playbackRecording - recording * @param {object} playbackRecording - recording
...@@ -112,7 +117,7 @@ class ExperimentExecutionService extends HttpService { ...@@ -112,7 +117,7 @@ class ExperimentExecutionService extends HttpService {
experimentID, experimentID,
privateExperiment, privateExperiment,
brainProcesses, brainProcesses,
server, serverID,
serverConfiguration, serverConfiguration,
reservation, reservation,
playbackRecording, playbackRecording,
...@@ -158,7 +163,7 @@ class ExperimentExecutionService extends HttpService { ...@@ -158,7 +163,7 @@ class ExperimentExecutionService extends HttpService {
.then((simulation) => { .then((simulation) => {
SimulationService.instance.initConfigFiles(serverURL, simulation.simulationID) SimulationService.instance.initConfigFiles(serverURL, simulation.simulationID)
.then(() => { .then(() => {
let simulationURL = 'esv-private/experiment-view/' + server + '/' + experimentID + '/' + let simulationURL = 'esv-private/experiment-view/' + serverID + '/' + experimentID + '/' +
privateExperiment + '/' + simulation.simulationID; privateExperiment + '/' + simulation.simulationID;
resolve(simulationURL); resolve(simulationURL);
ExperimentExecutionService.instance.emit(ExperimentExecutionService.EVENTS.START_EXPERIMENT, undefined); ExperimentExecutionService.instance.emit(ExperimentExecutionService.EVENTS.START_EXPERIMENT, undefined);
...@@ -205,18 +210,34 @@ class ExperimentExecutionService extends HttpService { ...@@ -205,18 +210,34 @@ class ExperimentExecutionService extends HttpService {
if (!data || !data.state) { if (!data || !data.state) {
return Promise.reject(); return Promise.reject();
} }
switch (data.state) {
case EXPERIMENT_STATE.CREATED: //CREATED --(initialize)--> PAUSED --(stop)--> STOPPED // CREATED --(initialize)--> PAUSED --(stop)--> STOPPED
if (data.state === EXPERIMENT_STATE.CREATED) {
return updateSimulationState(EXPERIMENT_STATE.INITIALIZED).then( return updateSimulationState(EXPERIMENT_STATE.INITIALIZED).then(
_.partial(updateSimulationState, EXPERIMENT_STATE.STOPPED) _.partial(updateSimulationState, EXPERIMENT_STATE.STOPPED)
); );
case EXPERIMENT_STATE.STARTED: //STARTED --(stop)--> STOPPED }
case EXPERIMENT_STATE.PAUSED: //PAUSED --(stop)--> STOPPED // STARTED/PAUSED/HALTED --(stop)--> STOPPED
case EXPERIMENT_STATE.HALTED: //HALTED --(stop)--> FAILED else if (data.state === EXPERIMENT_STATE.STARTED ||
data.state === EXPERIMENT_STATE.PAUSED ||
data.state === EXPERIMENT_STATE.HALTED) {
return updateSimulationState(EXPERIMENT_STATE.STOPPED); return updateSimulationState(EXPERIMENT_STATE.STOPPED);
default:
return Promise.reject();
} }
return Promise.reject();
/*switch (data.state) {
case EXPERIMENT_STATE.CREATED: //CREATED --(initialize)--> PAUSED --(stop)--> STOPPED
return updateSimulationState(EXPERIMENT_STATE.INITIALIZED).then(
_.partial(updateSimulationState, EXPERIMENT_STATE.STOPPED)
);
case EXPERIMENT_STATE.STARTED: //STARTED --(stop)--> STOPPED
case EXPERIMENT_STATE.PAUSED: //PAUSED --(stop)--> STOPPED
case EXPERIMENT_STATE.HALTED: //HALTED --(stop)--> FAILED
return updateSimulationState(EXPERIMENT_STATE.STOPPED);
default:
return Promise.reject();
}*/
}); });
/*eslint-enable camelcase*/ /*eslint-enable camelcase*/
}) })
...@@ -231,4 +252,9 @@ ExperimentExecutionService.EVENTS = Object.freeze({ ...@@ -231,4 +252,9 @@ ExperimentExecutionService.EVENTS = Object.freeze({
STOP_EXPERIMENT: 'STOP_EXPERIMENT' STOP_EXPERIMENT: 'STOP_EXPERIMENT'
}); });
ExperimentExecutionService.ERRORS = Object.freeze({
LAUNCH_FATAL_ERROR: 'failed to launch experiment, encountered a fatal error',
LAUNCH_NO_SERVERS_LEFT: 'failed to launch experiment, no available server could successfully start it'
});
export default ExperimentExecutionService; export default ExperimentExecutionService;
import _ from 'lodash';
import { Subject, timer } from 'rxjs';
import { switchMap, filter, map, multicast } from 'rxjs/operators';
import ErrorHandlerService from '../../error-handler-service.js'; import ErrorHandlerService from '../../error-handler-service.js';
import { HttpService } from '../../http-service.js'; import { HttpService } from '../../http-service.js';
import endpoints from '../../proxy/data/endpoints.json'; import endpoints from '../../proxy/data/endpoints.json';
import config from '../../../config.json'; import config from '../../../config.json';
const proxyServerURL = `${config.api.proxy.url}${endpoints.proxy.server.url}`; 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}`; const availableServersURL = `${config.api.proxy.url}${endpoints.proxy.availableServers.url}`;
let _instance = null; let _instance = null;
const SINGLETON_ENFORCER = Symbol(); 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. * Service handling server resources for simulating experiments.
*/ */
...@@ -31,9 +22,10 @@ class ServerResourcesService extends HttpService { ...@@ -31,9 +22,10 @@ class ServerResourcesService extends HttpService {
this.availableServers = []; this.availableServers = [];
this.startUpdates(); this.startUpdates();
window.onbeforeunload = () => { window.addEventListener('beforeunload', (event) => {
this.stopUpdates(); this.stopUpdates();
}; event.returnValue = '';
});
} }
static get instance() { static get instance() {
...@@ -48,17 +40,11 @@ class ServerResourcesService extends HttpService { ...@@ -48,17 +40,11 @@ class ServerResourcesService extends HttpService {
* Start polling updates. * Start polling updates.
*/ */
startUpdates() { startUpdates() {
this.clusterAvailabilityObservable = this._createSlurmMonitorObservable();
this.clusterAvailabilitySubscription = this.clusterAvailabilityObservable.subscribe(
availability => (clusterAvailability = availability)
);
this.getServerAvailability(true);
this.intervalGetServerAvailability = setInterval( this.intervalGetServerAvailability = setInterval(
() => { () => {
this.getServerAvailability(true); this.getServerAvailability(true);
}, },
INTERVAL_POLL_SERVER_AVAILABILITY ServerResourcesService.CONSTANTS.INTERVAL_POLL_SERVER_AVAILABILITY
); );
} }
...@@ -66,30 +52,17 @@ class ServerResourcesService extends HttpService { ...@@ -66,30 +52,17 @@ class ServerResourcesService extends HttpService {
* Stop polling updates. * Stop polling updates.
*/ */
stopUpdates() { stopUpdates() {
this.clusterAvailabilitySubscription && this.clusterAvailabilitySubscription.unsubscribe();
this.intervalGetServerAvailability && clearInterval(this.intervalGetServerAvailability); 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. * Return a list of available servers for starting simulations.
* @param {boolean} forceUpdate force an update * @param {boolean} forceUpdate force an update
* @returns {Array} A list of available servers. * @returns {Array} A list of available servers.
*/ */
getServerAvailability(forceUpdate = false) { async getServerAvailability(forceUpdate = false) {
if (!this.availableServers || forceUpdate) { if (!this.availableServers || forceUpdate) {
let update = async () => { this.availableServers = await (await this.httpRequestGET(availableServersURL)).json();
let response = await this.httpRequestGET(availableServersURL);
this.availableServers = await response.json();
};
update();
this.emit(ServerResourcesService.EVENTS.UPDATE_SERVER_AVAILABILITY, this.availableServers); this.emit(ServerResourcesService.EVENTS.UPDATE_SERVER_AVAILABILITY, this.availableServers);
} }
...@@ -108,32 +81,14 @@ class ServerResourcesService extends HttpService { ...@@ -108,32 +81,14 @@ class ServerResourcesService extends HttpService {
}) })
.catch(ErrorHandlerService.instance.displayServerHTTPError); .catch(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({ ServerResourcesService.EVENTS = Object.freeze({
UPDATE_SERVER_AVAILABILITY: 'UPDATE_SERVER_AVAILABILITY' UPDATE_SERVER_AVAILABILITY: 'UPDATE_SERVER_AVAILABILITY'
}); });
ServerResourcesService.CONSTANTS = Object.freeze({
INTERVAL_POLL_SERVER_AVAILABILITY: 3000
});
export default ServerResourcesService; export default ServerResourcesService;
...@@ -15,6 +15,15 @@ const experimentsUrl = `${config.api.proxy.url}${proxyEndpoint.storage.experimen ...@@ -15,6 +15,15 @@ const experimentsUrl = `${config.api.proxy.url}${proxyEndpoint.storage.experimen
jest.setTimeout(3 * ExperimentStorageService.CONSTANTS.INTERVAL_POLL_EXPERIMENTS); jest.setTimeout(3 * ExperimentStorageService.CONSTANTS.INTERVAL_POLL_EXPERIMENTS);
let onWindowBeforeUnloadCb = undefined;
beforeEach(() => {
jest.spyOn(window, 'addEventListener').mockImplementation((event, cb) => {
if (event === 'beforeunload') {
onWindowBeforeUnloadCb = cb;
}
});
});
afterEach(() => { afterEach(() => {
jest.restoreAllMocks(); jest.restoreAllMocks();
}); });
...@@ -82,6 +91,15 @@ test('does automatic poll updates of experiment list which can be stopped', (don ...@@ -82,6 +91,15 @@ test('does automatic poll updates of experiment list which can be stopped', (don
}, ExperimentStorageService.CONSTANTS.INTERVAL_POLL_EXPERIMENTS); }, ExperimentStorageService.CONSTANTS.INTERVAL_POLL_EXPERIMENTS);
}); });
test('should stop polling updates when window is unloaded', async () => {
let service = ExperimentStorageService.instance;
expect(onWindowBeforeUnloadCb).toBeDefined();
jest.spyOn(service, 'stopUpdates');
onWindowBeforeUnloadCb({});
expect(service.stopUpdates).toHaveBeenCalled();
});
test('gets a thumbnail image for experiments', async () => { test('gets a thumbnail image for experiments', async () => {
let experiment = MockExperiments[0]; let experiment = MockExperiments[0];
const imageBlob = await ExperimentStorageService.instance.getThumbnail(experiment.name, const imageBlob = await ExperimentStorageService.instance.getThumbnail(experiment.name,
......
...@@ -12,8 +12,6 @@ const storageImportExperiment = `${config.api.proxy.url}${endpoints.proxy.storag ...@@ -12,8 +12,6 @@ const storageImportExperiment = `${config.api.proxy.url}${endpoints.proxy.storag
let _instance = null; let _instance = null;
const SINGLETON_ENFORCER = Symbol(); const SINGLETON_ENFORCER = Symbol();
const INTERVAL_POLL_EXPERIMENTS = 3000;
/** /**
* Service that fetches the template experiments list from the proxy given * Service that fetches the template experiments list from the proxy given
* that the user has authenticated successfully. * that the user has authenticated successfully.
...@@ -49,7 +47,7 @@ class ExperimentStorageService extends HttpService { ...@@ -49,7 +47,7 @@ class ExperimentStorageService extends HttpService {
() => { () => {
this.getExperiments(true); this.getExperiments(true);
}, },
INTERVAL_POLL_EXPERIMENTS ExperimentStorageService.CONSTANTS.INTERVAL_POLL_EXPERIMENTS
); );
} }
...@@ -268,7 +266,7 @@ ExperimentStorageService.EVENTS = Object.freeze({ ...@@ -268,7 +266,7 @@ ExperimentStorageService.EVENTS = Object.freeze({
}); });
ExperimentStorageService.CONSTANTS = Object.freeze({ ExperimentStorageService.CONSTANTS = Object.freeze({
INTERVAL_POLL_EXPERIMENTS: INTERVAL_POLL_EXPERIMENTS INTERVAL_POLL_EXPERIMENTS: 3000
}); });
export default ExperimentStorageService; export default ExperimentStorageService;
...@@ -30,10 +30,10 @@ class NrpAnalyticsService { ...@@ -30,10 +30,10 @@ class NrpAnalyticsService {
options.value = _.toInteger(options.value); options.value = _.toInteger(options.value);
} }
return NrpUserService.instance.getCurrentUser().then((user) => { return NrpUserService.instance.getCurrentUser().then((user) => {
var extendedOptions = _.extend(options, { /*var extendedOptions = _.extend(options, {
label: user.displayName label: user.displayName
}); });
//$analytics.eventTrack(actionName, extendedOptions); $analytics.eventTrack(actionName, extendedOptions);*/
console.error('implement $analytics.eventTrack(actionName, extendedOptions)'); console.error('implement $analytics.eventTrack(actionName, extendedOptions)');
}); });
} }
......
...@@ -40,8 +40,7 @@ class NrpUserService extends HttpService { ...@@ -40,8 +40,7 @@ class NrpUserService extends HttpService {
* @returns {promise} Request for the user * @returns {promise} Request for the user
*/ */
async getUser(userID) { async getUser(userID) {
let response = await this.httpRequestGET(this.IDENTITY_BASE_URL + '/' + userID); return await (await this.httpRequestGET(this.IDENTITY_BASE_URL + '/' + userID)).json();
return response.json();
} }
/** /**
...@@ -62,8 +61,7 @@ class NrpUserService extends HttpService { ...@@ -62,8 +61,7 @@ class NrpUserService extends HttpService {
*/ */
async getCurrentUser() { async getCurrentUser() {
if (!this.currentUser) { if (!this.currentUser) {
let response = await this.httpRequestGET(this.IDENTITY_ME_URL); this.currentUser = await (await this.httpRequestGET(this.IDENTITY_ME_URL)).json();
this.currentUser = response.json();
} }
return this.currentUser; return this.currentUser;
......
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