-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Propagate root keypair changes to domains external to KeyManager
#312
Comments
Some notes regarding the recent discussion about observer and event emitter. The problem with using event emitter is that handlers become asynchronously asynchronous. Explained here: https://stackoverflow.com/questions/67961514/event-emitter-emit-in-sequence-or-in-parallel-and-behaviour-when-they-are-async In order to simplify testing and also ensure that side-effects can be controlled, it's better for when the keypair changes that all observers also complete their side-effects. However the loose coupling of event emitter is also desirable, especially by not propagating exceptions to the subject call site. Therefore it's a good idea to create our own "Observer" system that combines properties of event emitter and observer pattern. So basically we can create a Here is some pseudo code: class Observer {
handlers: Array<[() => Promise<void>, ((e: Error) => Promise<void>)?]> = [];
registerTopic (topic, f, onFailure) {
const handlers = this.handlers.get(topic);
handlers.push([f, onFailure]);
}
async updateTopic(topic) {
const handlers = this.handlers.get(topic);
// Change this to Promise.all instead, so that they are all run simultaneously
for (const [f, onFailure] of fs) {
try {
await f();
} catch (e) {
if (onFailure) {
await onFailure(e);
} else {
await this.handleError(e);
}
}
}
}
async handleError (e) {
this.logger.error(e);
}
} This way, when say The Exceptions that are thrown are however not thrown to the Then this The reason for not propagating these exceptions to the emission site, is that those exceptions are not part of This merges the 2 patterns together. It looks like somebody has done something similar here: Both are pretty old implementations and aren't in TS, so I think we build our own, and have a look at those for inspiration. The |
I've been testing out extending the Anyway I found a bug in upstream node, which will be solved by this PR: nodejs/node#41414 So right now |
I've prototyped a working "EventBus" that is an extension of import { EventEmitter } from 'events';
class EventBus extends EventEmitter {
protected kCapture: symbol;
constructor (...args: ConstructorParameters<typeof EventEmitter>) {
super(...args);
// EventEmitter's captureRejections option is only accessible through a private symbol
// Here we augment the construction and save it as a property
const symbols = Object.getOwnPropertySymbols(this);
this.kCapture = symbols[0];
}
public async emitAsync(event: string | symbol, ...args: Array<any>): Promise<boolean> {
const listeners = this.rawListeners(event);
if (listeners.length < 1) {
return false;
}
let result;
try {
for (const listener of listeners) {
result = listener.apply(this, args);
await result;
}
} catch (e) {
if (!this[this.kCapture]) {
throw e;
}
// The capture rejections mechanism only applies to promises
if (!(result instanceof Promise)) {
throw e;
}
// Queues error handling asynchronously to avoid bubbling up rejections
// This matches the behaviour of EventEmitter which uses `process.nextTick`
queueMicrotask(() => {
if (typeof this[EventEmitter.captureRejectionSymbol] === 'function') {
this[EventEmitter.captureRejectionSymbol](e, event, ...args);
} else {
// Disable the capture rejections mechanism to avoid infinite loop
const prev = this[this.kCapture];
// If the error handler throws, it results in `uncaughtException`
try {
this[this.kCapture] = false;
this.emit('error', e);
} finally {
this[this.kCapture] = prev;
}
}
});
}
return true;
}
} It turns out that the behaviour of error handling is quite complex. Some notes:
For now we can use |
Example usage: async function main () {
const e = new EventBus({
captureRejections: true
});
e.on('error', (e) => {
console.log('ERROR HANDLED', e);
});
e.on('sync', async () => {
console.log('1');
});
e.on('sync', async () => {
throw new Error('sync error');
});
e.on('sync', async () => {
console.log(2);
});
console.log('EMITTING');
const result = await e.emitAsync('sync');
console.log('result', result);
}
main(); Note that this must be run like this However it should work fine in our jest testing. |
@emmacasolin Some valid tests for |
Also please write down the plan regarding this usage, and what handlers you'll be registering and whether they will be asynchronous, and how this should impact testing... Note that getting the node id is still "pull-based" so that isn't affected by this new "push-based" mechanism. I can imagine the ability to push events as a stream to the GUI now as things are updated. |
Plan for using the
|
I was thinking that all event emission and registration/handling can be done in Instead domains expose "hookpoints" via callbacks. For example during the creation of Then Now in // get this exported out of the `events` index.ts` as well
import { EventBus, captureRejectionSymbol } from '../events';
const events = new EventBus({ captureRejections: true });
events[Symbol.for('nodejs.rejection')] = (err, event, ...args) => {
// log out the error here
};
const keyManager = await KeyManager.createKeyManager({
...,
rootKeyPairChange: (keyPair) => {
await events.emitAsync(keyUtils.eventRootKeyPairChange, keyPair);
}
}
events.on(keyUtils.eventRootKeyPairChange, async (keyPair) => {
await domain1.update(...);
await domain2.update(...);
}); Then subsequently register handlers for each event too. This keeps all of the event logic in One might ask if we are using hookpoints, why use the eventbus at all, just dispense with it and just call the updating functions directly in the hook point. This is a good point. One of the reasons to use eventbus is for "decentralised" event logic, where each domain emits events and then listens for events from other domains. This is best used when the domains are separated packages, or software components that are managed by separate people/teams. Whereas in PK, it's one single application codebase. So here we could just do direct calls, but we create an eventbus in case in the future, we are separating into separate components. So it's just a bit of future proofing. Note that you should switch on |
@tegefaulkes any thoughts on how nodegraph should be updated in relation to the root keypair changing based on this comment #312 (comment) |
From what I can tell, you just need to call |
Doing a quick test in a this.events.on(keysUtils.eventRootKeyPairChange, async (keyChangeData: RootKeyPairChangeData) => {
await this.status.updateNodeId(keyChangeData.nodeId);
await this.nodeManager.refreshBuckets();
this.fwdProxy.setTLSConfig(keyChangeData.tlsConfig);
this.revProxy.setTLSConfig(keyChangeData.tlsConfig);
this.grpcServerClient.setTLSConfig(keyChangeData.tlsConfig);
}); And then this is what gets logged in the terminal when renewing the key pair in a test:
I put I copied this example #312 (comment) for the most part, but the only thing I'm unsure about is what this line is for and what I should be putting inside it: events[Symbol.for('nodejs.rejection')] = (err, event, ...args) => {
// log out the error here
}; Will now start writing some proper tests to check that the event bus is working as expected. |
Implementation and tests have now been pushed up here: c394331. I've written tests for the |
Nice work! I suggest adding logger info messages at the beginning and end of your event handler, so we can know that those updating messages are grouped together. As for the error handler, all you need to do is log out the error. That's the only thing you can do. The It's also possible we don't bother doing that if those errors bubble up to root exception handlers. This requires some testing. In our // Both synchronous and asynchronous errors are handled
process.once('unhandledRejection', this.errorHandler);
process.once('uncaughtException', this.errorHandler); This means even if you don't Oh I just realised that the For example:
So this requires some planning:
|
From memory, other nodes will need to use the new node Id eventually. However this is done in a "lazy way". Which is that old node ids are still valid, but when the establish a connection, they are meant to be updated with the new node id if it has been changed. There are some cases where we need to push out to everybody, that may include all the nodes that are part of the gestalt, or in the DHT, we need to propagate updates, other node's node graph are now out of date. This is where #190 may be relevant. Just something to think about, I don't think we have fully specced out this behaviour yet. |
At the moment, event handlers are being registered in PolykeyAgent's |
If there's an error in start, all event handlers should be explicitly deregistered. You could do this with |
Make sure to do that when Otherwise event listeners would be duplicated. |
I'm not sure what would be considered a relevant error for keypair propagation, since any error caught by the event handler will have already halted the propagation to domains. In this case would it be better to just do something like this.events.on(
keysUtils.eventRootKeyPairChange,
async (keyChangeData: RootKeyPairChangeData) => {
try {
this.logger.info('Propagating root keypair change');
await this.status.updateNodeId(keyChangeData.nodeId);
await this.nodeManager.refreshBuckets();
this.fwdProxy.setTLSConfig(keyChangeData.tlsConfig);
this.revProxy.setTLSConfig(keyChangeData.tlsConfig);
this.grpcServerClient.setTLSConfig(keyChangeData.tlsConfig);
this.logger.info('Propagated root keypair change');
} catch (e) {
this.logger.warn('Keypair could not be propagated to all domains');
throw new errors.ErrorKeyPairPropagate(e.message, {
errno: e.errno,
syscall: e.syscall,
code: e.code,
path: e.path,
});
}
},
); |
I think it needs to be done bottom up first. You want to catch exceptions for each method that you're calling like Remember this order:
If PolykeyAgent cannot recover from the error, don't bother catching it on Your eventbus error handler should instead use |
// use the captureRejectionsSymbol instead of Symbol.for
this.events[captureRejectionSymbol] = (err, event, ...args) => {
this.logger.error(...);
throw err;
}
this.events.on(
keysUtils.eventRootKeyPairChange,
async () => { ... }
); |
Attempting to catch errors this way is working fine in tests (i.e. this.events[captureRejectionSymbol] = (err, event, ...args) => {
this.logger.error(...);
throw err;
} |
No the reason is that there's an upstream bug in NodeJS that prevents |
Yeah I just remembered that and figured it was probably the issue. Will do some more tests to make sure though |
Changing the node id will impact communications between:
Under Agent to Agent, this is going through the forward and reverse proxies, and also the Under CLI to Agent and GUI to Agent, they are using direct TLS on the GRPCClient. Existing GRPCClient connections should continue to work and not be interrupted by the TLS change. I believe I had tests for this under See that the
So we need tests to ensure this is the case. One for Then we would extend these tests into I remember a prior issue where I wrote that there 3 modes of connecting to the agent. Details: This means there was another mode where during CLI to agent, the client provides a cert as well... but we scrapped that idea simply because the client may not be on the same host as the agent, and therefore not have access to the certificate files. That's why we built the session authentication logic. |
This has been implemented here: fc0e83b. However a new issue should be created to address the "Test communications when root key pair is renewed and NodeID is changed". @emmacasolin please create new issue for #311 (comment) and the above comment, and then close this issue, while referencing this issue in the new issue for additional context. |
Closing this issue. Comms testing to be tracked here: #317 |
Specification
When the root keypair is changed, this needs to be propagated to other places that rely on knowing this information. In particular, the new Node Id generated by a new keypair needs to be propagated to the
NodeManager
andStatus
, among other places. This can be achieved either through the use of the Observer Pattern, or by using Event Emitters (preferred due to looser coupling).Additional context
Tasks
KeyManager
that allows events to be propagated to dependent classesThe text was updated successfully, but these errors were encountered: