Extend error handling in FluencePeer (#98)

This commit is contained in:
Pavel 2021-11-09 14:37:44 +03:00 committed by GitHub
parent 337a3f45de
commit aa21abe465
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 214 additions and 103 deletions

14
package-lock.json generated
View File

@ -10,7 +10,7 @@
"license": "Apache-2.0",
"dependencies": {
"@chainsafe/libp2p-noise": "4.0.0",
"@fluencelabs/avm": "0.15.4",
"@fluencelabs/avm": "0.16.6",
"async": "3.2.0",
"base64-js": "1.5.1",
"bs58": "4.0.1",
@ -646,9 +646,9 @@
}
},
"node_modules/@fluencelabs/avm": {
"version": "0.15.4",
"resolved": "https://registry.npmjs.org/@fluencelabs/avm/-/avm-0.15.4.tgz",
"integrity": "sha512-NLZDq83ocJ1Helm0D8kPMSSkjxH0y+Tujg0px773zjIShbh3jgiJOjAW1xCYgTt9K0LqepjP0bWX4/8nUZfr7g==",
"version": "0.16.6",
"resolved": "https://registry.npmjs.org/@fluencelabs/avm/-/avm-0.16.6.tgz",
"integrity": "sha512-RDNXW/VYAXh+E7B7+S4pTTc/1IcvtlID2xyBs/3TDlxkjbVxM7+vMcFL6cJZOzZZl+3oAWXL3ibDhE5Elcq6ug==",
"dependencies": {
"base64-js": "1.5.1"
}
@ -8689,9 +8689,9 @@
}
},
"@fluencelabs/avm": {
"version": "0.15.4",
"resolved": "https://registry.npmjs.org/@fluencelabs/avm/-/avm-0.15.4.tgz",
"integrity": "sha512-NLZDq83ocJ1Helm0D8kPMSSkjxH0y+Tujg0px773zjIShbh3jgiJOjAW1xCYgTt9K0LqepjP0bWX4/8nUZfr7g==",
"version": "0.16.6",
"resolved": "https://registry.npmjs.org/@fluencelabs/avm/-/avm-0.16.6.tgz",
"integrity": "sha512-RDNXW/VYAXh+E7B7+S4pTTc/1IcvtlID2xyBs/3TDlxkjbVxM7+vMcFL6cJZOzZZl+3oAWXL3ibDhE5Elcq6ug==",
"requires": {
"base64-js": "1.5.1"
}

View File

@ -21,7 +21,7 @@
"license": "Apache-2.0",
"dependencies": {
"@chainsafe/libp2p-noise": "4.0.0",
"@fluencelabs/avm": "0.15.4",
"@fluencelabs/avm": "0.16.6",
"async": "3.2.0",
"base64-js": "1.5.1",
"bs58": "4.0.1",

View File

@ -1,5 +1,6 @@
import { FluencePeer } from '../../index';
import { Particle } from '../../internal/Particle';
import { handleTimeout } from '../../internal/utils';
import { registerHandlersHelper } from '../util';
describe('Avm spec', () => {
@ -21,10 +22,9 @@ describe('Avm spec', () => {
resolve(res);
},
},
_timeout: reject,
});
peer.internals.initiateParticle(particle);
peer.internals.initiateParticle(particle, handleTimeout(reject));
});
// assert
@ -61,10 +61,9 @@ describe('Avm spec', () => {
}
},
},
_timeout: reject,
});
peer.internals.initiateParticle(particle);
peer.internals.initiateParticle(particle, handleTimeout(reject));
});
// assert

View File

@ -2,6 +2,8 @@ import { Fluence, FluencePeer } from '../../..';
import { Particle } from '../../../internal/Particle';
import { registerHandlersHelper } from '../../util';
import { callMeBack, registerHelloWorld } from './gen1';
import { callFunction } from '../../../internal/compilerSupport/v2';
import { handleTimeout } from '../../../internal/utils';
describe('Compiler support infrastructure tests', () => {
it('Compiled code for function should work', async () => {
@ -78,10 +80,9 @@ describe('Compiler support infrastructure tests', () => {
resolve(val);
},
},
_timeout: reject,
});
Fluence.getPeer().internals.initiateParticle(particle);
Fluence.getPeer().internals.initiateParticle(particle, handleTimeout(reject));
});
// assert
@ -166,9 +167,8 @@ describe('Compiler support infrastructure tests', () => {
resolve(val);
},
},
_timeout: reject,
});
anotherPeer.internals.initiateParticle(particle);
anotherPeer.internals.initiateParticle(particle, handleTimeout(reject));
});
// assert
@ -177,4 +177,33 @@ describe('Compiler support infrastructure tests', () => {
await anotherPeer.stop();
});
it('Should throw error if particle with incorrect AIR script is initiated', async () => {
// arrange;
const anotherPeer = new FluencePeer();
await anotherPeer.start();
// act
const action = callFunction(
[anotherPeer],
{
functionName: 'dontcare',
argDefs: [],
returnType: { tag: 'void' },
names: {
relay: '-relay-',
getDataSrv: 'getDataSrv',
callbackSrv: 'callbackSrv',
responseSrv: 'callbackSrv',
responseFnName: 'response',
errorHandlingSrv: 'errorHandlingSrv',
errorFnName: 'error',
},
},
'incorrect air script',
);
// assert
await expect(action).rejects.toMatch(/incorrect air script/);
});
});

View File

@ -1,7 +1,7 @@
import { Multiaddr } from 'multiaddr';
import { nodes } from '../connection';
import { Fluence, FluencePeer, setLogLevel } from '../../index';
import { checkConnection } from '../../internal/utils';
import { checkConnection, doNothing, handleTimeout } from '../../internal/utils';
import { Particle } from '../../internal/Particle';
import { registerHandlersHelper } from '../util';
@ -121,10 +121,9 @@ describe('Typescript usage suite', () => {
reject(error);
},
},
_timeout: reject,
});
anotherPeer.internals.initiateParticle(particle);
anotherPeer.internals.initiateParticle(particle, handleTimeout(reject));
});
// assert
@ -169,7 +168,7 @@ describe('Typescript usage suite', () => {
)
`;
const particle = Particle.createNew(script);
await peer1.internals.initiateParticle(particle);
await peer1.internals.initiateParticle(particle, doNothing);
// assert
const res = await resMakingPromise;
@ -309,10 +308,9 @@ describe('Typescript usage suite', () => {
resolve(res);
},
},
_timeout: reject,
});
anotherPeer.internals.initiateParticle(particle);
anotherPeer.internals.initiateParticle(particle, handleTimeout(reject));
});
// assert
@ -368,10 +366,9 @@ describe('Typescript usage suite', () => {
reject(error);
},
},
_timeout: reject,
});
anotherPeer.internals.initiateParticle(particle);
anotherPeer.internals.initiateParticle(particle, handleTimeout(reject));
});
// assert
@ -381,7 +378,6 @@ describe('Typescript usage suite', () => {
it('Should not crash if an error ocurred in user-defined handler', async () => {
// arrange;
setLogLevel('trace');
await anotherPeer.start();
// act
@ -405,10 +401,9 @@ describe('Typescript usage suite', () => {
reject(error);
},
},
_timeout: reject,
});
anotherPeer.internals.initiateParticle(particle);
anotherPeer.internals.initiateParticle(particle, handleTimeout(reject));
});
// assert
@ -417,6 +412,22 @@ describe('Typescript usage suite', () => {
});
});
it('Should throw error if particle is initiated on a stopped peer', async () => {
// arrange;
const stoppedPeer = new FluencePeer();
// act
const action = () => {
const script = `(null)`;
const particle = Particle.createNew(script);
stoppedPeer.internals.initiateParticle(particle, doNothing);
};
// assert
await expect(action).toThrow('Cannot initiate new particle: peer is not initialized');
});
it.skip('Should throw correct error when the client tries to send a particle not to the relay', async () => {
// arrange;
await anotherPeer.start({ connectTo: nodes[0] });
@ -439,7 +450,7 @@ describe('Typescript usage suite', () => {
},
});
anotherPeer.internals.initiateParticle(particle);
anotherPeer.internals.initiateParticle(particle, doNothing);
});
// assert
@ -468,10 +479,9 @@ async function callIncorrectService(peer: FluencePeer): Promise<string[]> {
reject(error);
},
},
_timeout: reject,
});
peer.internals.initiateParticle(particle);
peer.internals.initiateParticle(particle, handleTimeout(reject));
});
return promise;

View File

@ -3,14 +3,10 @@ import { Particle } from '../internal/Particle';
import { MakeServiceCall } from '../internal/utils';
export const registerHandlersHelper = (peer: FluencePeer, particle: Particle, handlers) => {
const { _timeout, ...rest } = handlers;
if (_timeout) {
peer.internals.regHandler.timeout(particle.id, _timeout);
}
for (let serviceId in rest) {
for (let fnName in rest[serviceId]) {
for (let serviceId in handlers) {
for (let fnName in handlers[serviceId]) {
// of type [args] => result
const h = rest[serviceId][fnName];
const h = handlers[serviceId][fnName];
peer.internals.regHandler.forParticle(particle.id, serviceId, fnName, MakeServiceCall(h));
}
}

View File

@ -44,7 +44,7 @@ export const Fluence = {
},
/**
* Uninitializes the default peer: stops all the underltying workflows, stops the Aqua VM
* Un-initializes the default peer: stops all the underlying workflows, stops the Aqua VM
* and disconnects from the Fluence network
*/
stop: (): Promise<void> => {

View File

@ -27,13 +27,14 @@ import { CallServiceData, CallServiceResult, GenericCallServiceHandler, ResultCo
import { CallServiceHandler as LegacyCallServiceHandler } from './compilerSupport/LegacyCallServiceHandler';
import { PeerIdB58 } from './commonTypes';
import { FluenceConnection } from './FluenceConnection';
import { Particle } from './Particle';
import { Particle, ParticleExecutionStage, ParticleQueueItem } from './Particle';
import { KeyPair } from './KeyPair';
import { createInterpreter, dataToString } from './utils';
import { filter, pipe, Subject, tap } from 'rxjs';
import { RequestFlow } from './compilerSupport/v1';
import log from 'loglevel';
import { defaultServices } from './defaultServices';
import { instanceOf } from 'ts-pattern';
/**
* Node of the Fluence network specified as a pair of node's multiaddr and it's peer id
@ -202,7 +203,7 @@ export class FluencePeer {
peerId: this._keyPair.Libp2pPeerId,
relayAddress: connectToMultiAddr,
dialTimeoutMs: config.dialTimeoutMs,
onIncomingParticle: (p) => this._incomingParticles.next(p),
onIncomingParticle: (p) => this._incomingParticles.next({ particle: p, onStageChange: () => {} }),
});
await this._connect();
@ -226,7 +227,6 @@ export class FluencePeer {
this._particleSpecificHandlers.clear();
this._commonHandlers.clear();
this._timeoutHandlers.clear();
}
// internal api
@ -240,7 +240,11 @@ export class FluencePeer {
* Initiates a new particle execution starting from local peer
* @param particle - particle to start execution of
*/
initiateParticle: (particle: Particle): void => {
initiateParticle: (particle: Particle, onStageChange: (stage: ParticleExecutionStage) => void): void => {
if (!this.getStatus().isInitialized) {
throw 'Cannot initiate new particle: peer is not initialized';
}
if (particle.initPeerId === undefined) {
particle.initPeerId = this.getStatus().peerId;
}
@ -249,8 +253,12 @@ export class FluencePeer {
particle.ttl = this._defaultTTL;
}
this._incomingParticles.next(particle);
this._incomingParticles.next({
particle: particle,
onStageChange: onStageChange,
});
},
/**
* Register Call Service handler functions
*/
@ -283,12 +291,6 @@ export class FluencePeer {
psh.set(serviceFnKey(serviceId, fnName), handler);
},
/**
* Register handler which will be called upon particle timeout
*/
timeout: (particleId: string, handler: () => void) => {
this._timeoutHandlers.set(particleId, handler);
},
},
/**
@ -303,7 +305,15 @@ export class FluencePeer {
timeout: request.timeout,
});
this.internals.initiateParticle(particle);
this.internals.initiateParticle(particle, (stage) => {
if (stage.stage === 'interpreterError') {
request?.error(stage.errorMessage);
}
if (stage.stage === 'expired') {
request?.timeout();
}
});
},
/**
@ -334,14 +344,13 @@ export class FluencePeer {
// Queues for incoming and outgoing particles
private _incomingParticles = new Subject<Particle>();
private _outgoingParticles = new Subject<Particle>();
private _incomingParticles = new Subject<ParticleQueueItem>();
private _outgoingParticles = new Subject<ParticleQueueItem>();
// Call service handler
private _particleSpecificHandlers = new Map<string, Map<string, GenericCallServiceHandler>>();
private _commonHandlers = new Map<string, GenericCallServiceHandler>();
private _timeoutHandlers = new Map<string, () => void>();
// Internal peer state
@ -351,15 +360,18 @@ export class FluencePeer {
private _connection: FluenceConnection;
private _interpreter: AirInterpreter;
private _timeouts: Array<NodeJS.Timeout> = [];
private _particleQueues = new Map<string, Subject<Particle>>();
private _particleQueues = new Map<string, Subject<ParticleQueueItem>>();
private _startParticleProcessing() {
this._incomingParticles
.pipe(
tap((x) => x.logTo('debug', 'particle received:')),
tap((x) => {
x.particle.logTo('debug', 'particle received:');
}),
filterExpiredParticles(this._expireParticle.bind(this)),
)
.subscribe((p) => {
.subscribe((item) => {
const p = item.particle;
let particlesQueue = this._particleQueues.get(p.id);
if (!particlesQueue) {
@ -367,34 +379,35 @@ export class FluencePeer {
this._particleQueues.set(p.id, particlesQueue);
const timeout = setTimeout(() => {
this._expireParticle(p.id);
this._expireParticle(item);
}, p.actualTtl());
this._timeouts.push(timeout);
}
particlesQueue.next(p);
particlesQueue.next(item);
});
this._outgoingParticles.subscribe((p) => {
this._connection.sendParticle(p);
this._outgoingParticles.subscribe(async (item) => {
await this._connection.sendParticle(item.particle);
item.onStageChange({ stage: 'sent' });
});
}
private _expireParticle(particleId: string) {
log.debug(`particle ${particleId} has expired. Deleting particle-related queues and handlers`);
private _expireParticle(item: ParticleQueueItem) {
const particleId = item.particle.id;
log.debug(
`particle ${particleId} has expired after ${item.particle.ttl}. Deleting particle-related queues and handlers`,
);
this._particleQueues.delete(particleId);
const timeoutHandler = this._timeoutHandlers.get(particleId);
if (timeoutHandler) {
timeoutHandler();
}
this._particleSpecificHandlers.delete(particleId);
this._timeoutHandlers.delete(particleId);
item.onStageChange({ stage: 'expired' });
}
private _createParticlesProcessingQueue() {
let particlesQueue = new Subject<Particle>();
let particlesQueue = new Subject<ParticleQueueItem>();
let prevData: Uint8Array = Buffer.from([]);
particlesQueue
@ -402,28 +415,42 @@ export class FluencePeer {
// force new line
filterExpiredParticles(this._expireParticle.bind(this)),
)
.subscribe((x) => {
const result = runInterpreter(this.getStatus().peerId, this._interpreter, x, prevData);
.subscribe((item) => {
const particle = item.particle;
const result = runInterpreter(this.getStatus().peerId, this._interpreter, particle, prevData);
prevData = Buffer.from(result.data);
// Do not continue if there was an error in particle interpretation
if (isInterpretationSuccessful(result)) {
item.onStageChange({ stage: 'interpreterError', errorMessage: result.errorMessage });
return;
}
setTimeout(() => {
item.onStageChange({ stage: 'interpreted' });
}, 0);
const newData = Buffer.from(result.data);
prevData = newData;
// send particle further if requested
if (result.nextPeerPks.length > 0) {
const newParticle = x.clone();
newParticle.data = prevData;
this._outgoingParticles.next(newParticle);
const newParticle = particle.clone();
newParticle.data = newData;
this._outgoingParticles.next({ ...item, particle: newParticle });
}
// execute call requests if needed
// and put particle with the results back to queue
if (result.callRequests.length > 0) {
this._execCallRequests(x, result.callRequests).then((callResults) => {
const newParticle = x.clone();
this._execCallRequests(particle, result.callRequests).then((callResults) => {
const newParticle = particle.clone();
newParticle.callResults = callResults;
newParticle.data = Buffer.from([]);
particlesQueue.next(newParticle);
particlesQueue.next({ ...item, particle: newParticle });
});
} else {
item.onStageChange({ stage: 'localWorkDone' });
}
});
@ -547,6 +574,10 @@ export class FluencePeer {
private _legacyCallServiceHandler: LegacyCallServiceHandler;
}
function isInterpretationSuccessful(result: InterpreterResult) {
return result.retCode !== 0 || result?.errorMessage?.length > 0;
}
function serviceFnKey(serviceId: string, fnName: string) {
return `${serviceId}/${fnName}`;
}
@ -582,17 +613,22 @@ function runInterpreter(
const toLog: any = { ...interpreterResult };
toLog.data = dataToString(toLog.data);
log.debug('Interpreter result: ', toLog);
if (isInterpretationSuccessful(interpreterResult)) {
log.debug('Interpreter result: ', toLog);
} else {
log.error('Interpreter failed: ', toLog);
}
return interpreterResult;
}
function filterExpiredParticles(onParticleExpiration: (particleId: string) => void) {
function filterExpiredParticles(onParticleExpiration: (item: ParticleQueueItem) => void) {
return pipe(
tap((p: Particle) => {
if (p.hasExpired()) {
onParticleExpiration(p.id);
tap((item: ParticleQueueItem) => {
if (item.particle.hasExpired()) {
onParticleExpiration(item);
}
}),
filter((x: Particle) => !x.hasExpired()),
filter((x: ParticleQueueItem) => !x.particle.hasExpired()),
);
}

View File

@ -140,6 +140,19 @@ export class Particle {
}
}
export type ParticleExecutionStage =
| { stage: 'received' }
| { stage: 'interpreted' }
| { stage: 'interpreterError'; errorMessage: string }
| { stage: 'localWorkDone' }
| { stage: 'sent' }
| { stage: 'expired' };
export interface ParticleQueueItem {
particle: Particle;
onStageChange: (state: ParticleExecutionStage) => void;
}
function genUUID() {
return uuidv4();
}

View File

@ -68,12 +68,28 @@ export enum ResultCodes {
*/
export interface ParticleContext {
/**
* The particle ID
* The identifier of particle which triggered the call
*/
particleId: string;
/**
* The peer id which created the particle
*/
initPeerId: PeerIdB58;
/**
* Particle's timestamp when it was created
*/
timestamp: number;
/**
* Time to live in milliseconds. The time after the particle should be expired
*/
ttl: number;
/**
* Particle's signature
*/
signature: string;
}

View File

@ -320,22 +320,25 @@ export function callFunction(rawFnArgs: Array<any>, def: FunctionCallDef, script
};
});
// registering handler for particle timeout
peer.internals.regHandler.timeout(particle.id, () => {
reject(`Request timed out for ${def.functionName}`);
});
peer.internals.initiateParticle(particle, (stage) => {
// If function is void, then it's completed when one of the two conditions is met:
// 1. The particle is sent to the network (state 'sent')
// 2. All CallRequests are executed, e.g., all variable loading and local function calls are completed (state 'localWorkDone')
if (def.returnType.tag === 'void' && (stage.stage === 'sent' || stage.stage === 'localWorkDone')) {
resolve(undefined);
}
peer.internals.initiateParticle(particle);
if (stage.stage === 'expired') {
reject(`Request timed out after ${particle.ttl} for ${def.functionName}`);
}
if (stage.stage === 'interpreterError') {
reject(`Script interpretation failed for ${def.functionName}: ${stage.errorMessage}`);
}
});
});
// if the function has void type we should resolve immediately for API symmetry with non-void types
// to help with debugging we are returning a promise which can be used to track particle errors
// we cannot return a bare promise because JS will lift it, so returning an array with the promise
if (def.returnType.tag === 'void') {
return Promise.resolve([promise]);
} else {
return promise;
}
return promise;
}
/**

View File

@ -18,7 +18,7 @@ import { AirInterpreter, LogLevel as AvmLogLevel } from '@fluencelabs/avm';
import log from 'loglevel';
import { CallServiceData, CallServiceResult, CallServiceResultType, ResultCodes } from './commonTypes';
import { AvmLoglevel, FluencePeer } from './FluencePeer';
import { Particle } from './Particle';
import { Particle, ParticleExecutionStage } from './Particle';
export const createInterpreter = (logLevel: AvmLoglevel): Promise<AirInterpreter> => {
const logFn = (level: AvmLogLevel, msg: string) => {
@ -53,6 +53,14 @@ export const MakeServiceCall = (fn: (args: any[]) => CallServiceResultType) => {
};
};
export const handleTimeout = (fn: Function) => (stage: ParticleExecutionStage) => {
if (stage.stage === 'expired') {
fn();
}
};
export const doNothing = (stage: ParticleExecutionStage) => {};
/**
* Checks the network connection by sending a ping-like request to relay node
* @param { FluenceClient } peer - The Fluence Client instance.
@ -127,11 +135,12 @@ export const checkConnection = async (peer: FluencePeer, ttl?: number): Promise<
}),
);
peer.internals.regHandler.timeout(particle.id, () => {
reject('particle timed out');
});
peer.internals.initiateParticle(particle);
peer.internals.initiateParticle(
particle,
handleTimeout(() => {
reject('particle timed out');
}),
);
});
try {