Skip to content
This repository has been archived by the owner on Nov 6, 2020. It is now read-only.

Work around WS in UI #3587

Merged
merged 6 commits into from
Nov 23, 2016
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 122 additions & 12 deletions js/src/api/transport/ws/ws.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,33 @@ export default class Ws extends JsonRpcBase {
this._token = token;
this._messages = {};

this._connecting = true;
this._connecting = false;
this._connected = false;
this._lastError = null;
this._autoConnect = false;
this._autoConnect = true;
this._retries = 0;
this._reconnectTimeoutId = null;

this._connect();
}

updateToken (token) {
this._token = token;
this._autoConnect = false;
this._autoConnect = true;

this._connect();
}

_connect () {
if (this._connecting) {
return;
}

if (this._reconnectTimeoutId) {
window.clearTimeout(this._reconnectTimeoutId);
this._reconnectTimeoutId = null;
}

const time = parseInt(new Date().getTime() / 1000, 10);
const sha3 = keccak_256(`${this._token}:${time}`);
const hash = `${sha3}_${time}`;
Expand All @@ -53,6 +65,7 @@ export default class Ws extends JsonRpcBase {
this._ws.onopen = null;
this._ws.onclose = null;
this._ws.onmessage = null;
this._ws.close();
this._ws = null;
}

Expand All @@ -65,32 +78,101 @@ export default class Ws extends JsonRpcBase {
this._ws.onopen = this._onOpen;
this._ws.onclose = this._onClose;
this._ws.onmessage = this._onMessage;

// Get counts in dev mode
if (process.env.NODE_ENV === 'development') {
this._count = 0;
this._lastCount = {
timestamp: Date.now(),
count: 0
};

window.setInterval(() => {
const n = this._count - this._lastCount.count;
const t = (Date.now() - this._lastCount.timestamp) / 1000;
const s = Math.round(1000 * n / t) / 1000;

if (this._debug) {
console.log('::parityWS', `speed: ${s} req/s`, `count: ${this._count}`);
}
}, 5000);

window._parityWS = this;
}
}

_checkNodeUp () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't really belong in the WS transport layer, the purpose it to manage the connection, not rely on how the rest is stitched together. Here we are now assuming that there is always a capable /api/ping available.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better now with the HEAD /, however we are assuming that the content is served from the same location as the WS now in the WS interface. Very specific...

const url = process.env.PARITY_URL || window.location.host;

return fetch(
`http://${url}/api/ping`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:8180 doesn't have /api/ping

{ method: 'HEAD' }
)
.then((r) => {
return r.status === 200;
}, () => {
return false;
})
.catch(() => false);
}

_onOpen = (event) => {
console.log('ws:onOpen', event);
this._connected = true;
this._connecting = false;
this._autoConnect = true;
this._retries = 0;

Object.keys(this._messages)
.filter((id) => this._messages[id].queued)
.forEach(this._send);
}

_onClose = (event) => {
console.log('ws:onClose', event);
this._connected = false;
this._connecting = false;

if (this._autoConnect) {
setTimeout(() => this._connect(), 500);
}
this._checkNodeUp()
.then((up) => {
// If the connection has been closed and the node
// is up, it means we have a wrong token
// Event code 1006 for WS means there is an error
// (not just closed by server)
if (up && event.code === 1006) {
event.status = 403;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dangerous, but better than what we have atm. (Apart from the up check that doesn't belong)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the thing is that there is no way to know why a WS connection has been closed. It's like this in the specs. I will just remove this and put the checkNodeUp method in the secureApi

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeap. That is the issue - if we had a 403 like gets displayed in the console, it would be so much easier.

}

this._lastError = event;

if (this._autoConnect) {
const timeout = this.retryTimeout;

const time = timeout < 1000
? Math.round(timeout) + 'ms'
: (Math.round(timeout / 10) / 100) + 's';

console.log('ws:onClose', `trying again in ${time}...`);

this._reconnectTimeoutId = setTimeout(() => {
this._connect();
}, timeout);

return;
}

console.log('ws:onClose', event);
});
}

_onError = (event) => {
console.error('ws:onError', event);
this._lastError = event;
// Only print error if the WS is connected
// ie. don't print if error == closed
window.setTimeout(() => {
if (this._connected) {
console.error('ws:onError', event);
this._lastError = event;
}
}, 50);
}

_onMessage = (event) => {
Expand Down Expand Up @@ -127,11 +209,16 @@ export default class Ws extends JsonRpcBase {
_send = (id) => {
const message = this._messages[id];

message.queued = !this._connected;

if (this._connected) {
this._ws.send(message.json);
if (process.env.NODE_ENV === 'development') {
this._count++;
}

return this._ws.send(message.json);
}

message.queued = !this._connected;
message.timestamp = Date.now();
}

execute (method, ...params) {
Expand Down Expand Up @@ -159,4 +246,27 @@ export default class Ws extends JsonRpcBase {
get lastError () {
return this._lastError;
}

/**
* Exponential Timeout for Retries
*
* @see http://dthain.blogspot.de/2009/02/exponential-backoff-in-distributed.html
*/
get retryTimeout () {
// R between 1 and 2
const R = Math.random() + 1;
// Initial timeout (100ms)
const T = 100;
// Exponential Factor
const F = 2;
// Max timeout (4s)
const M = 4000;
// Current number of retries
const N = this._retries;

// Increase retries number
this._retries++;

return Math.min(R * T * Math.pow(F, N), M);
}
}
40 changes: 27 additions & 13 deletions js/src/contracts/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,39 @@ export default class Registry {
this._api = api;
this._contracts = [];
this._instance = null;
this._fetching = false;
this._queue = [];

this.getInstance();
}

getInstance () {
return new Promise((resolve, reject) => {
if (this._instance) {
resolve(this._instance);
return;
}
if (this._instance) {
return Promise.resolve(this._instance);
}

this._api.parity
.registryAddress()
.then((address) => {
this._instance = this._api.newContract(abis.registry, address).instance;
resolve(this._instance);
})
.catch(reject);
});
if (this._fetching) {
return new Promise((resolve) => {
this._queue.push({ resolve });
});
}

this._fetching = true;

return this._api.parity
.registryAddress()
.then((address) => {
this._fetching = false;
this._instance = this._api.newContract(abis.registry, address).instance;

this._queue.forEach((queued) => {
queued.resolve(this._instance);
});

this._queue = [];

return this._instance;
});
}

getContract (_name) {
Expand Down
13 changes: 4 additions & 9 deletions js/src/redux/providers/balances.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { getBalances, getTokens } from './balancesActions';
import { setAddressImage } from './imagesActions';

import Contracts from '../../contracts';
import * as abis from '../../contracts/abi';

import imagesEthereum from '../../../assets/images/contracts/ethereum-black-64x64.png';
Expand Down Expand Up @@ -84,15 +85,9 @@ export default class Balances {
return Promise.resolve(this._tokenreg);
}

return this._api.parity
.registryAddress()
.then((registryAddress) => {
const registry = this._api.newContract(abis.registry, registryAddress);

return registry.instance.getAddress.call({}, [this._api.util.sha3('tokenreg'), 'A']);
})
.then((tokenregAddress) => {
const tokenreg = this._api.newContract(abis.tokenreg, tokenregAddress);
return Contracts.get().tokenReg
.getContract()
.then((tokenreg) => {
this._tokenreg = tokenreg;
this.attachToTokens();

Expand Down
34 changes: 20 additions & 14 deletions js/src/redux/providers/status.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ export default class Status {
* @see src/views/Connection/connection.js
*/
_shouldPing = () => {
const { isConnected, isConnecting } = this._apiStatus;
return isConnecting || !isConnected;
const { isConnected } = this._apiStatus;
return !isConnected;
}

_stopPollPing = () => {
Expand Down Expand Up @@ -102,7 +102,7 @@ export default class Status {
}, timeout);
};

fetch('/', { method: 'HEAD' })
fetch('/api/ping', { method: 'HEAD' })
Copy link
Contributor

@jacogr jacogr Nov 23, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anything under //api/* is not available on :8180, needs to be /. You cannot connect to :8080 either since to get the port you first need the API up via dappsUrl/dappsPort.

.then((response) => dispatch(!!response.ok))
.catch(() => dispatch(false));
}
Expand All @@ -119,7 +119,7 @@ export default class Status {

_pollStatus = () => {
const nextTimeout = (timeout = 1000) => {
setTimeout(this._pollStatus, timeout);
setTimeout(() => this._pollStatus(), timeout);
};

const { isConnected, isConnecting, needsToken, secureToken } = this._api;
Expand All @@ -134,7 +134,8 @@ export default class Status {
const gotReconnected = !this._apiStatus.isConnected && apiStatus.isConnected;

if (gotReconnected) {
this._pollLongStatus();
this._pollLongStatus(true);
this._store.dispatch(statusCollection({ isPingable: true }));
}

if (!isEqual(apiStatus, this._apiStatus)) {
Expand Down Expand Up @@ -175,13 +176,12 @@ export default class Status {
this._store.dispatch(statusCollection(status));
this._status = status;
}

nextTimeout();
})
.catch((error) => {
console.error('_pollStatus', error);
nextTimeout(250);
});

nextTimeout();
}

/**
Expand Down Expand Up @@ -223,7 +223,11 @@ export default class Status {
* fetched every 30s just in case, and whenever
* the client got reconnected.
*/
_pollLongStatus = () => {
_pollLongStatus = (newConnection = false) => {
if (!this._api.isConnected) {
return;
}

const nextTimeout = (timeout = 30000) => {
if (this._longStatusTimeoutId) {
clearTimeout(this._longStatusTimeoutId);
Expand All @@ -242,7 +246,7 @@ export default class Status {
this._api.parity.netChain(),
this._api.parity.netPort(),
this._api.parity.rpcSettings(),
this._api.parity.enode()
newConnection ? Promise.resolve(null) : this._api.parity.enode()
])
.then(([
clientVersion, defaultExtraData, netChain, netPort, rpcSettings, enode
Expand All @@ -255,21 +259,23 @@ export default class Status {
netChain,
netPort,
rpcSettings,
enode,
isTest
};

if (enode) {
longStatus.enode = enode;
}

if (!isEqual(longStatus, this._longStatus)) {
this._store.dispatch(statusCollection(longStatus));
this._longStatus = longStatus;
}

nextTimeout();
})
.catch((error) => {
console.error('_pollLongStatus', error);
nextTimeout(250);
});

nextTimeout(newConnection ? 5000 : 30000);
}

_pollLogs = () => {
Expand Down
Loading