Skip to content

Commit

Permalink
Update to Kallichore 0.1.9 (#4988)
Browse files Browse the repository at this point in the history
This change updates Positron to use 0.1.9 of the Kallichore supervisor.
This update includes the following features:

- Rework "start kernel" RPC to not return until kernel has actually
started, and include stderr/stdout if it fails to start or connect.
Addresses #4960.
- Busy/idle time reporting, intended for use in Posit Workbench.
- 20 second timeout to avoid hanging when waiting for a ZeroMQ socket
connect.
- Bearer token auth; RPCs that change state now check the token (already
supplied by Positron).

### QA Notes

This is mostly internal improvements; #4690 has an example that can be
used to create startup failures.
  • Loading branch information
jmcphers authored Oct 16, 2024
1 parent d56e898 commit e95dbf3
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 19 deletions.
2 changes: 1 addition & 1 deletion extensions/kallichore-adapter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
},
"positron": {
"binaryDependencies": {
"kallichore": "0.1.8"
"kallichore": "0.1.11"
}
},
"dependencies": {
Expand Down
67 changes: 52 additions & 15 deletions extensions/kallichore-adapter/src/KallichoreSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as os from 'os';
import * as path from 'path';
import * as fs from 'fs';
import { JupyterKernelExtra, JupyterKernelSpec, JupyterLanguageRuntimeSession } from './jupyter-adapter';
import { ActiveSession, DefaultApi, HttpError, InterruptMode, NewSession, Status } from './kcclient/api';
import { ActiveSession, DefaultApi, HttpError, InterruptMode, NewSession, StartupError, Status } from './kcclient/api';
import { JupyterMessage } from './jupyter/JupyterMessage';
import { JupyterRequest } from './jupyter/JupyterRequest';
import { KernelInfoRequest } from './jupyter/KernelInfoRequest';
Expand Down Expand Up @@ -643,25 +643,64 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession {
* @returns The kernel info for the session.
*/
async start(): Promise<positron.LanguageRuntimeInfo> {
try {
// Attempt to start the session
await this.tryStart();
} catch (err) {
if (err instanceof HttpError && err.statusCode === 500) {
// When the server returns a 500 error, it means the startup
// failed. In this case the API returns a structured startup
// error we can use to report the problem with more detail.
const startupErr = err.body;
let message = startupErr.error.message;
if (startupErr.output) {
message += `\n${startupErr.output}`;
}
const event: positron.LanguageRuntimeExit = {
runtime_name: this.runtimeMetadata.runtimeName,
exit_code: startupErr.exit_code ?? 0,
reason: positron.RuntimeExitReason.StartupFailed,
message
};
this._exit.fire(event);
} else {
// This indicates that startup failed due to a problem on the
// client side. We still need to report an exit so that the UI
// treats the runtime as exited.

// Attempt to extract a message from the error, or just
// stringify it if it's not an Error
const message =
err instanceof Error ? err.message : JSON.stringify(err);
const event: positron.LanguageRuntimeExit = {
runtime_name: this.runtimeMetadata.runtimeName,
exit_code: 0,
reason: positron.RuntimeExitReason.StartupFailed,
message
};
this._exit.fire(event);
}

this.onStateChange(positron.RuntimeState.Exited);
throw err;
}

return this.getKernelInfo();
}

/**
* Attempts to start the session; returns a promise that resolves when the
* session is ready to use.
*/
private async tryStart(): Promise<void> {
// Wait for the session to be established before connecting. This
// ensures either that we've created the session (if it's new) or that
// we've restored it (if it's not new).
await withTimeout(this._established.wait(), 2000, `Start failed: timed out waiting for session ${this.metadata.sessionId} to be established`);

// If it's a new session, wait for it to be created before connecting
if (this._new) {

// Wait for the session to start
try {
await this._api.startSession(this.metadata.sessionId);
} catch (err) {
if (err instanceof HttpError) {
throw new Error(err.body.message);
} else {
// Rethrow the error as-is if it's not an HTTP error
throw err;
}
}
await this._api.startSession(this.metadata.sessionId);
}

// Before connecting, check if we should attach to the session on
Expand Down Expand Up @@ -707,8 +746,6 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession {
this._state.fire(positron.RuntimeState.Ready);
}
}

return this.getKernelInfo();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ model/newSession.ts
model/newSession200Response.ts
model/serverStatus.ts
model/sessionList.ts
model/startupError.ts
model/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { NewSession } from '../model/newSession';
import { NewSession200Response } from '../model/newSession200Response';
import { ServerStatus } from '../model/serverStatus';
import { SessionList } from '../model/sessionList';
import { StartupError } from '../model/startupError';

import { ObjectSerializer, Authentication, VoidAuth, Interceptor } from '../model/models';
import { HttpBasicAuth, HttpBearerAuth, ApiKeyAuth, OAuth } from '../model/models';
Expand Down
18 changes: 18 additions & 0 deletions extensions/kallichore-adapter/src/kcclient/model/activeSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ export class ActiveSession {
'continuationPrompt': string;
'executionQueue': ExecutionQueue;
'status': Status;
/**
* The number of seconds the session has been idle, or 0 if the session is busy
*/
'idleSeconds': number;
/**
* The number of seconds the session has been busy, or 0 if the session is idle
*/
'busySeconds': number;

static discriminator: string | undefined = undefined;

Expand Down Expand Up @@ -145,6 +153,16 @@ export class ActiveSession {
"name": "status",
"baseName": "status",
"type": "Status"
},
{
"name": "idleSeconds",
"baseName": "idle_seconds",
"type": "number"
},
{
"name": "busySeconds",
"baseName": "busy_seconds",
"type": "number"
} ];

static getAttributeTypeMap() {
Expand Down
3 changes: 3 additions & 0 deletions extensions/kallichore-adapter/src/kcclient/model/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from './newSession';
export * from './newSession200Response';
export * from './serverStatus';
export * from './sessionList';
export * from './startupError';
export * from './status';

import * as fs from 'fs';
Expand All @@ -31,6 +32,7 @@ import { NewSession } from './newSession';
import { NewSession200Response } from './newSession200Response';
import { ServerStatus } from './serverStatus';
import { SessionList } from './sessionList';
import { StartupError } from './startupError';
import { Status } from './status';

/* tslint:disable:no-unused-variable */
Expand Down Expand Up @@ -58,6 +60,7 @@ let typeMap: {[index: string]: any} = {
"NewSession200Response": NewSession200Response,
"ServerStatus": ServerStatus,
"SessionList": SessionList,
"StartupError": StartupError,
}

export class ObjectSerializer {
Expand Down
18 changes: 18 additions & 0 deletions extensions/kallichore-adapter/src/kcclient/model/serverStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ export class ServerStatus {
'sessions': number;
'active': number;
'busy': boolean;
/**
* The number of seconds all sessions have been idle, or 0 if any session is busy
*/
'idleSeconds': number;
/**
* The number of seconds any session has been busy, or 0 if all sessions are idle
*/
'busySeconds': number;
'version': string;

static discriminator: string | undefined = undefined;
Expand All @@ -36,6 +44,16 @@ export class ServerStatus {
"baseName": "busy",
"type": "boolean"
},
{
"name": "idleSeconds",
"baseName": "idle_seconds",
"type": "number"
},
{
"name": "busySeconds",
"baseName": "busy_seconds",
"type": "number"
},
{
"name": "version",
"baseName": "version",
Expand Down
50 changes: 50 additions & 0 deletions extensions/kallichore-adapter/src/kcclient/model/startupError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Kallichore API
* Kallichore is a Jupyter kernel gateway and supervisor
*
* The version of the OpenAPI document: 1.0.0
* Contact: info@posit.co
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/

import { RequestFile } from './models';
import { ModelError } from './modelError';

export class StartupError {
/**
* The exit code of the process, if it exited
*/
'exitCode'?: number;
/**
* The output of the process (combined stdout and stderr) emitted during startup, if any
*/
'output'?: string;
'error': ModelError;

static discriminator: string | undefined = undefined;

static attributeTypeMap: Array<{name: string, baseName: string, type: string}> = [
{
"name": "exitCode",
"baseName": "exit_code",
"type": "number"
},
{
"name": "output",
"baseName": "output",
"type": "string"
},
{
"name": "error",
"baseName": "error",
"type": "ModelError"
} ];

static getAttributeTypeMap() {
return StartupError.attributeTypeMap;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -1695,9 +1695,13 @@ class PositronConsoleInstance extends Disposable implements IPositronConsoleInst
}

// Add a message explaining that the exit occurred, and why.
let message = this.formatExit(exit);
if (exit.message) {
message += `\n\n${exit.message}`;
}
const exited = new RuntimeItemExited(generateUuid(),
exit.reason,
this.formatExit(exit));
message);
this.addRuntimeItem(exited);

// Show restart button if crashed and user has disabled automatic restarts
Expand Down
13 changes: 11 additions & 2 deletions src/vs/workbench/services/runtimeSession/common/runtimeSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1007,8 +1007,17 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession
}));

this._register(session.onDidEndSession(async exit => {
// Note that we need to do this on the next tick since we need to
// ensure all the event handlers for the state change we are
// The session is no longer running, so if it's the active console
// session, clear it.
if (session.metadata.sessionMode === LanguageRuntimeSessionMode.Console) {
const consoleSession = this._consoleSessionsByLanguageId.get(session.runtimeMetadata.languageId);
if (consoleSession?.sessionId === session.sessionId) {
this._consoleSessionsByLanguageId.delete(session.runtimeMetadata.languageId);
}
}

// Note that we need to do the following on the next tick since we
// need to ensure all the event handlers for the state change we are
// currently processing have been called (i.e. everyone knows it has
// exited)
setTimeout(() => {
Expand Down

0 comments on commit e95dbf3

Please sign in to comment.