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

event based update of server availability

parent 05b9427b
No related branches found
No related tags found
No related merge requests found
......@@ -2974,6 +2974,11 @@
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
},
"arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
},
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
......@@ -4300,6 +4305,11 @@
}
}
},
"clsx": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz",
"integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA=="
},
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
......@@ -4646,6 +4656,11 @@
"sha.js": "^2.4.8"
}
},
"create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
},
"cross-fetch": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.6.tgz",
......@@ -5256,6 +5271,11 @@
}
}
},
"diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="
},
"diff-sequences": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz",
......@@ -10462,6 +10482,11 @@
}
}
},
"make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
},
"makeerror": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz",
......@@ -13396,6 +13421,15 @@
"workbox-webpack-plugin": "5.1.4"
}
},
"react-tabs": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-3.1.2.tgz",
"integrity": "sha512-OKS1l7QzSNcn+L2uFsxyGFHdXp9YsPGf/YOURWcImp7xLN36n0Wz+/j9HwlwGtlXCZexwshScR5BrcFbw/3P9Q==",
"requires": {
"clsx": "^1.1.0",
"prop-types": "^15.5.0"
}
},
"react-transition-group": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
......@@ -15709,6 +15743,19 @@
"resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz",
"integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA=="
},
"ts-node": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz",
"integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==",
"requires": {
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"source-map-support": "^0.5.17",
"yn": "3.1.1"
}
},
"ts-pnp": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz",
......@@ -17684,6 +17731,11 @@
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
},
"yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="
},
"yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
......
......@@ -16,6 +16,7 @@
"react-dom": "^17.0.1",
"react-router-dom": "5.2.0",
"react-scripts": "4.0.0",
"react-tabs": "3.1.2",
"roslib": "1.1.0",
"rxjs": "6.6.3",
"ts-node": "^9.1.1",
......@@ -66,7 +67,12 @@
"error",
"consistent"
],
"max-len": ["error", { "code": 120 }]
"max-len": [
"error",
{
"code": 120
}
]
}
},
"browserslist": {
......@@ -85,4 +91,4 @@
"jest-fetch-mock": "^3.0.3",
"msw": "^0.23.0"
}
}
\ No newline at end of file
}
......@@ -2,9 +2,9 @@ import React from 'react';
import timeDDHHMMSS from '../../utility/time-filter.js';
import ExperimentStorageService from '../../services/experiments/storage/experiment-storage-service.js';
import ExperimentExecutionService from '../../services/experiments/execution/experiment-execution-service.js';
import ExperimentServerService from '../../services/experiments/execution/experiment-server-service.js';
import './experiment-list-element.css';
import ExperimentServerService from '../../services/experiments/execution/experiment-server-service.js';
const CLUSTER_THRESHOLDS = {
UNAVAILABLE: 2,
......@@ -15,7 +15,7 @@ const SHORT_DESCRIPTION_LENGTH = 200;
export default class ExperimentListElement extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.state = {availableServers: []};
this.canLaunchExperiment = (this.props.experiment.private && this.props.experiment.owned) ||
!this.props.experiment.private;
......@@ -29,13 +29,27 @@ export default class ExperimentListElement extends React.Component {
let thumbnail = await ExperimentStorageService.instance.getThumbnail(
this.props.experiment.name,
this.props.experiment.configuration.thumbnail);
this.setState({ thumbnail: URL.createObjectURL(thumbnail) });
this.setState({
thumbnail: URL.createObjectURL(thumbnail)
});
document.addEventListener('mousedown', this.handleClickOutside);
this.onUpdateServerAvailability = (availableServers) => {
this.setState({availableServers: availableServers});
};
ExperimentServerService.instance.addListener(
ExperimentServerService.EVENTS.UPDATE_SERVER_AVAILABILITY,
this.onUpdateServerAvailability
);
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.handleClickOutside);
this.onUpdateServerAvailability && ExperimentServerService.instance.removeListener(
ExperimentServerService.EVENTS.UPDATE_SERVER_AVAILABILITY,
this.onUpdateServerAvailability
);
}
handleClickOutside(event) {
......@@ -45,14 +59,13 @@ export default class ExperimentListElement extends React.Component {
}
getAvailabilityInfo() {
const experiment = this.props.experiment;
const clusterAvailability = ExperimentServerService.instance.getClusterAvailability();
let status;
if (clusterAvailability && clusterAvailability.free > CLUSTER_THRESHOLDS.AVAILABLE) {
status = 'Available';
}
else if (!experiment.availableServers || experiment.availableServers.length === 0) {
else if (!this.state.availableServers || this.state.availableServers.length === 0) {
status = 'Unavailable';
}
else {
......@@ -60,20 +73,19 @@ export default class ExperimentListElement extends React.Component {
}
let cluster = `Cluster availability: ${clusterAvailability.free} / ${clusterAvailability.total}`;
let backends = `Backends: ${experiment.availableServers.length}`;
let backends = `Backends: ${this.state.availableServers.length}`;
return `${status}\n${cluster}\n${backends}`;
}
getServerStatusClass() {
const experiment = this.props.experiment;
const clusterAvailability = ExperimentServerService.instance.getClusterAvailability();
let status = '';
if (clusterAvailability && clusterAvailability.free > CLUSTER_THRESHOLDS.AVAILABLE) {
status = 'server-status-available';
}
else if (!experiment.availableServers || experiment.availableServers.length === 0) {
else if (!this.state.availableServers || this.state.availableServers.length === 0) {
status = 'server-status-unavailable';
}
else {
......@@ -86,7 +98,7 @@ export default class ExperimentListElement extends React.Component {
render() {
const exp = this.props.experiment;
const config = this.props.experiment.configuration;
const pageState = this.props.pageState;
const pageState = this.props.pageState; //TODO: to be removed, migrate to services
return (
<div className='list-entry-wrapper flex-container left-right'
......@@ -136,27 +148,29 @@ export default class ExperimentListElement extends React.Component {
return exp.id === pageState.selected;
}}>
<div className='btn-group' role='group' >
{this.canLaunchExperiment && exp.availableServers.length > 0 &&
{this.canLaunchExperiment && this.state.availableServers.length > 0 &&
exp.configuration.experimentFile && exp.configuration.bibiConfSrc
? <button onClick={() => {
return ExperimentExecutionService.instance.startingExperiment === exp.id ||
ExperimentExecutionService.instance.startNewExperiment(exp, false);
ExperimentExecutionService.instance.startNewExperiment(exp, false);
}}
disabled={pageState.startingExperiment === exp.id || pageState.deletingExperiment}
//TODO: adjust disabled state to be reactive
disabled={ExperimentExecutionService.instance.startingExperiment === exp
|| pageState.deletingExperiment}
className='btn btn-default' >
<i className='fa fa-plus'></i> Launch
</button>
: null}
{this.canLaunchExperiment && exp.availableServers.length === 0
? <button className='btn btn-default disabled enable-tooltip'
{this.canLaunchExperiment && this.state.availableServers.length === 0
? <button disabled={this.canLaunchExperiment && this.state.availableServers.length === 0}
className='btn btn-default disabled enable-tooltip'
title='Sorry, no available servers.'>
<i className='fa fa-plus'></i> Launch
</button>
: null}
{this.canLaunchExperiment && config.brainProcesses > 1 &&
exp.availableServers.length > 0 &&
this.state.availableServers.length > 0 &&
exp.configuration.experimentFile && exp.configuration.bibiConfSrc
? <button className='btn btn-default'>
......@@ -164,7 +178,7 @@ export default class ExperimentListElement extends React.Component {
</button>
: null}
{this.canLaunchExperiment && exp.availableServers.length > 1 &&
{this.canLaunchExperiment && this.state.availableServers.length > 1 &&
exp.configuration.experimentFile && exp.configuration.bibiConfSrc
? <button className='btn btn-default' >
......@@ -193,7 +207,7 @@ export default class ExperimentListElement extends React.Component {
</button>
: null}
{/* Join button */}
{/* Simulations button */}
{this.canLaunchExperiment && exp.joinableServers.length > 0
? <button className='btn btn-default' >
<i className='fa fa-sign-in'></i> Simulations »
......
......@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
import UserMenu from '../user-menu/user-menu.js';
import ExperimentStorageService from '../../services/experiments/storage/experiment-storage-service.js';
import ExperimentServerService from '../../services/experiments/execution/experiment-server-service.js';
import ExperimentListElement from './experiment-list-element.js';
......@@ -60,7 +61,8 @@ export default class ExperimentList extends React.Component {
{this.state.experiments.map(experiment => {
return (
<li key={experiment.id} className='nostyle'>
<ExperimentListElement experiment={experiment} pageState={this.state.pageState} />
<ExperimentListElement experiment={experiment}
pageState={this.state.pageState} />
</li>
);
}
......
......@@ -47,10 +47,11 @@ class ExperimentExecutionService extends HttpService {
this.startingExperiment = experiment;
console.info(ExperimentServerService.instance.getServerAvailability(true));
let fatalErrorOccurred = false,
serversToTry = experiment.devServer
? [experiment.devServer]
: experiment.availableServers.map(s => s.id);
: ExperimentServerService.instance.getServerAvailability(true).map(s => s.id);
let brainProcesses = launchSingleMode ? 1 : experiment.configuration.brainProcesses;
......
......@@ -11,12 +11,14 @@ 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 SLURM_MONITOR_POLL_INTERVAL = 5000;
const SERVER_AVAILABILITY_POLL_INTERVAL = 5000;
let clusterAvailability = { free: 'N/A', total: 'N/A' };
/**
......@@ -49,7 +51,12 @@ class ExperimentServerService extends HttpService {
.pipe(map(({ free, nodes }) => ({ free, total: nodes[3] })))
.pipe(multicast(new Subject())).refCount();
this.listOfAvailableServers = [];
this.startUpdates();
window.onbeforeunload = () => {
this.stopUpdates();
};
}
static get instance() {
......@@ -60,6 +67,13 @@ class ExperimentServerService extends HttpService {
return _instance;
}
/**
* Get available servers.
*/
get availableServers() {
return this.listOfAvailableServers;
}
/**
* Start polling updates.
*/
......@@ -67,14 +81,21 @@ class ExperimentServerService extends HttpService {
this.clusterAvailabilitySubscription = this.clusterAvailabilityObservable.subscribe(
availability => (clusterAvailability = availability)
);
this.timerPollServerAvailability = setInterval(
() => {
this.getServerAvailability(true);
},
SERVER_AVAILABILITY_POLL_INTERVAL
);
}
/**
* Stop polling updates.
*/
//TODO: find proper place to call
stopUpdates() {
this.clusterAvailabilitySubscription && this.clusterAvailabilitySubscription.unsubscribe();
this.timerPollServerAvailability && clearInterval(this.timerPollServerAvailability);
}
/**
......@@ -85,6 +106,19 @@ class ExperimentServerService extends HttpService {
return clusterAvailability;
}
getServerAvailability(forceUpdate = false) {
if (!this.listOfAvailableServers || forceUpdate) {
let update = async () => {
let response = await this.httpRequestGET(availableServersURL);
this.listOfAvailableServers = await response.json();
};
update();
this.emit(ExperimentServerService.EVENTS.UPDATE_SERVER_AVAILABILITY, this.listOfAvailableServers);
}
return this.listOfAvailableServers;
}
/**
* Get the server config for a given server ID.
* @param {string} serverID - ID of the server
......@@ -204,4 +238,8 @@ class ExperimentServerService extends HttpService {
};
}
ExperimentServerService.EVENTS = Object.freeze({
UPDATE_SERVER_AVAILABILITY: 'UPDATE_SERVER_AVAILABILITY'
});
export default ExperimentServerService;
......@@ -3,7 +3,6 @@ import { HttpService } from '../../http-service.js';
import endpoints from '../../proxy/data/endpoints.json';
import config from '../../../config.json';
const storageExperimentsURL = `${config.api.proxy.url}${endpoints.proxy.storage.experiments.url}`;
const availableServersURL = `${config.api.proxy.url}${endpoints.proxy.availableServers.url}`;
let _instance = null;
const SINGLETON_ENFORCER = Symbol();
......@@ -35,8 +34,9 @@ class ExperimentStorageService extends HttpService {
*
* @return experiments - the list of template experiments
*/
async getExperiments() {
if (!this.experiments) {
async getExperiments(forceUpdate = false) {
console.info('getExperiments');
if (!this.experiments || forceUpdate) {
let response = await this.httpRequestGET(storageExperimentsURL);
this.experiments = await response.json();
this.sortExperiments();
......@@ -78,11 +78,7 @@ class ExperimentStorageService extends HttpService {
}
async fillExperimentDetails() {
let response = await this.httpRequestGET(availableServersURL);
let availableServers = await response.json();
this.experiments.forEach(exp => {
exp.availableServers = availableServers;
if (!exp.configuration.brainProcesses && exp.configuration.bibiConfSrc) {
exp.configuration.brainProcesses = 1;
}
......
import {EventEmitter} from 'events';
import AuthenticationService from './authentication-service.js';
/**
......@@ -5,11 +8,13 @@ import AuthenticationService from './authentication-service.js';
* If children need other options they can override the options or the
* http verb (GET, POST, PUT etc) functions.
*/
export class HttpService {
export class HttpService extends EventEmitter {
/**
* Create a simple http request object with default options, default method is GET
*/
constructor() {
super();
this.options = {
method: 'GET', // *GET, POST, PUT, DELETE, etc.
mode: 'cors', // no-cors, *cors, same-origin
......
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