-
Notifications
You must be signed in to change notification settings - Fork 0
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
Ungraceful closures of streams used cancel
or abort
with no reason - remove all of these, and we can rely on timeouts instead
#27
Comments
All instances of cancel or abort without a reason
@ready(new rpcErrors.ErrorMissingCaller())
public async unaryCaller<I extends JSONValue, O extends JSONValue>(
method: string,
parameters: I,
ctx: Partial<ContextTimedInput> = {},
): Promise<O> {
const callerInterface = await this.duplexStreamCaller<I, O>(method, ctx);
const reader = callerInterface.readable.getReader();
const writer = callerInterface.writable.getWriter();
try {
await writer.write(parameters);
const output = await reader.read();
if (output.done) {
throw new rpcErrors.ErrorMissingCaller('Missing response', {
cause: ctx.signal?.reason,
});
}
await reader.cancel();
await writer.close();
return output.value;
} finally {
// Attempt clean up, ignore errors if already cleaned up
await reader.cancel().catch(() => {});
await writer.close().catch(() => {});
}
} Instance 2 // Hooking up agnostic stream side
let rpcStream: RPCStream<Uint8Array, Uint8Array>;
const streamFactoryProm = this.streamFactory({ signal, timer });
try {
rpcStream = await Promise.race([streamFactoryProm, abortRaceProm.p]);
} catch (e) {
cleanUp();
void streamFactoryProm.then((stream) =>
stream.cancel(ErrorRPCStreamEnded),
);
throw e;
}
void timer.then(
() => {
rpcStream.cancel(
new rpcErrors.ErrorRPCTimedOut('RPC has timed out', {
cause: ctx.signal?.reason,
}),
);
},
() => {}, // Ignore cancellation error
);
// Deciding if we want to allow refreshing
// We want to refresh timer if none was provided
const refreshingTimer: Timer | undefined =
ctx.timer == null ? timer : undefined;
// Composing stream transforms and middleware
const metadata = {
...(rpcStream.meta ?? {}),
command: method,
};
const outputMessageTransformStream =
rpcUtils.clientOutputTransformStream<O>(metadata, refreshingTimer);
const inputMessageTransformStream = rpcUtils.clientInputTransformStream<I>(
method,
refreshingTimer,
);
const middleware = this.middlewareFactory(
{ signal, timer },
rpcStream.cancel,
metadata,
);
@ready(new rpcErrors.ErrorRPCHandlerFailed())
public handleStream(rpcStream: RPCStream<Uint8Array, Uint8Array>) {
// This will take a buffer stream of json messages and set up service
// handling for it.
// Constructing the PromiseCancellable for tracking the active stream
const abortController = new AbortController();
// Setting up timeout timer logic
const timer = new Timer({
delay: this.handlerTimeoutTime,
handler: () => {
abortController.abort(new rpcErrors.ErrorRPCTimedOut());
if (this.onTimeoutCallback) {
this.onTimeoutCallback();
}
},
});
const prom = (async () => {
const id = await this.idGen();
const headTransformStream = rpcUtilsMiddleware.binaryToJsonMessageStream(
rpcUtils.parseJSONRPCRequest,
);
// Transparent transform used as a point to cancel the input stream from
const passthroughTransform = new TransformStream<
Uint8Array,
Uint8Array
>();
const inputStream = passthroughTransform.readable;
const inputStreamEndProm = rpcStream.readable
.pipeTo(passthroughTransform.writable)
// Ignore any errors here, we only care that it ended
.catch(() => {});
void inputStream
// Allow us to re-use the readable after reading the first message
.pipeTo(headTransformStream.writable, {
preventClose: true,
preventCancel: true,
})
// Ignore any errors here, we only care that it ended
.catch(() => {});
const cleanUp = async (reason: any) => {
await inputStream.cancel(reason);
await rpcStream.writable.abort(reason);
await inputStreamEndProm;
timer.cancel(cleanupReason);
await timer.catch(() => {});
};
// Read a single empty value to consume the first message
const reader = headTransformStream.readable.getReader();
// Allows timing out when waiting for the first message
let headerMessage:
| ReadableStreamDefaultReadResult<JSONRPCRequest>
| undefined
| void;
try {
headerMessage = await Promise.race([
reader.read(),
timer.then(
() => undefined,
() => {},
),
]);
} catch (e) {
const newErr = new rpcErrors.ErrorRPCHandlerFailed(
'Stream failed waiting for header',
{ cause: e },
);
await inputStreamEndProm;
timer.cancel(cleanupReason);
await timer.catch(() => {});
this.dispatchEvent(
new rpcEvents.RPCErrorEvent({
detail: new rpcErrors.ErrorRPCOutputStreamError(
'Stream failed waiting for header',
{
cause: newErr,
},
),
}),
);
return;
}
// Downgrade back to the raw stream
await reader.cancel();
// There are 2 conditions where we just end here
// 1. The timeout timer resolves before the first message
// 2. the stream ends before the first message |
Discussion about RPCServer and RPCClient lifecycles: MatrixAI/Polykey#552 (comment)
Conclusion is:
|
Lines 217 to 236 in c885dc1
Right now this says And it needs to send a special reason for |
If the RPC handlers reject upon being aborted by |
Internal signal reasons are usually symbols. Not exception objects. |
cancel
or abort
that have no reason passed in
cancel
or abort
that have no reason passed incancel
or abort
that have no reason passed in - because this is bad practice and also we can rely on timeouts now
Ignore everything except than the OP. The OP has been rewritten to focus on what we need. |
cancel
or abort
that have no reason passed in - because this is bad practice and also we can rely on timeouts nowcancel
or abort
with no reason - remove all of these, and we can rely on timeouts instead
@amydevs is this also being done? |
This is still pending as there is still cancellations occurring without reasons in |
i don't think there are any places where There is one place where it is done: const reader = headTransformStream.readable.getReader();
// Allows timing out when waiting for the first message
let headerMessage:
| ReadableStreamDefaultReadResult<JSONRPCRequest>
| undefined
| void;
try {
headerMessage = await Promise.race([
reader.read(),
timer.then(
() => undefined,
() => {},
),
]);
} catch (e) {
const newErr = new errors.ErrorRPCHandlerFailed(
'Stream failed waiting for header',
{ cause: e },
);
await inputStreamEndProm;
timer.cancel(cleanupReason);
await timer.catch(() => {});
this.dispatchEvent(
new events.RPCErrorEvent({
detail: new errors.ErrorRPCOutputStreamError(
'Stream failed waiting for header',
{ cause: newErr },
),
}),
);
return;
}
// Downgrade back to the raw stream
await reader.cancel(); But this does not matter, as the headTransformStream is Tee of the actual underlying stream that is meant to only parse the header of each RPC call. Hence, the cancel does not propagate to the parent stream. Therefore, this issue is considered no longer applicable. |
When you say it doesn't matter, should we even keep it? |
Specification
We cannot call
cancel
orabort
without reasons. This can causeundefined
errors, which is very difficult to debug.These are being used to ungracefully close streams, possibly in
RPCClient
andRPCServer
.This shouldn't be used anymore anyway because we can rely on timeouts instead, rather than ungracefully closing streams.
Additional Context
cancel
andabort
callsThe text was updated successfully, but these errors were encountered: