manager.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { Poll } from '@jupyterlab/coreutils';
  4. import { ArrayExt, IIterator, iter } from '@phosphor/algorithm';
  5. import { JSONExt } from '@phosphor/coreutils';
  6. import { ISignal, Signal } from '@phosphor/signaling';
  7. import { Kernel } from '../kernel';
  8. import { ServerConnection } from '../serverconnection';
  9. import { Session } from './session';
  10. /**
  11. * An implementation of a session manager.
  12. */
  13. export class SessionManager implements Session.IManager {
  14. /**
  15. * Construct a new session manager.
  16. *
  17. * @param options - The default options for each session.
  18. */
  19. constructor(options: SessionManager.IOptions = {}) {
  20. this.serverSettings =
  21. options.serverSettings || ServerConnection.makeSettings();
  22. // Initialize internal data.
  23. this._ready = Promise.all([this.requestRunning(), this.requestSpecs()])
  24. .then(_ => undefined)
  25. .catch(_ => undefined)
  26. .then(() => {
  27. if (this.isDisposed) {
  28. return;
  29. }
  30. this._isReady = true;
  31. });
  32. // Start model and specs polling with exponential backoff.
  33. this._pollModels = new Poll({
  34. auto: false,
  35. factory: () => this.requestRunning(),
  36. frequency: {
  37. interval: 10 * 1000,
  38. backoff: true,
  39. max: 300 * 1000
  40. },
  41. name: `@jupyterlab/services:SessionManager#models`,
  42. standby: options.standby || 'when-hidden'
  43. });
  44. this._pollSpecs = new Poll({
  45. auto: false,
  46. factory: () => this.requestSpecs(),
  47. frequency: {
  48. interval: 61 * 1000,
  49. backoff: true,
  50. max: 300 * 1000
  51. },
  52. name: `@jupyterlab/services:SessionManager#specs`,
  53. standby: options.standby || 'when-hidden'
  54. });
  55. void this.ready.then(() => {
  56. void this._pollModels.start();
  57. void this._pollSpecs.start();
  58. });
  59. }
  60. /**
  61. * A signal emitted when the kernel specs change.
  62. */
  63. get specsChanged(): ISignal<this, Kernel.ISpecModels> {
  64. return this._specsChanged;
  65. }
  66. /**
  67. * A signal emitted when the running sessions change.
  68. */
  69. get runningChanged(): ISignal<this, Session.IModel[]> {
  70. return this._runningChanged;
  71. }
  72. /**
  73. * A signal emitted when there is a connection failure.
  74. */
  75. get connectionFailure(): ISignal<this, Error> {
  76. return this._connectionFailure;
  77. }
  78. /**
  79. * Test whether the manager is disposed.
  80. */
  81. get isDisposed(): boolean {
  82. return this._isDisposed;
  83. }
  84. /**
  85. * The server settings of the manager.
  86. */
  87. readonly serverSettings: ServerConnection.ISettings;
  88. /**
  89. * Get the most recently fetched kernel specs.
  90. */
  91. get specs(): Kernel.ISpecModels | null {
  92. return this._specs;
  93. }
  94. /**
  95. * Test whether the manager is ready.
  96. */
  97. get isReady(): boolean {
  98. return this._isReady;
  99. }
  100. /**
  101. * A promise that fulfills when the manager is ready.
  102. */
  103. get ready(): Promise<void> {
  104. return this._ready;
  105. }
  106. /**
  107. * Dispose of the resources used by the manager.
  108. */
  109. dispose(): void {
  110. if (this.isDisposed) {
  111. return;
  112. }
  113. this._isDisposed = true;
  114. this._models.length = 0;
  115. this._pollModels.dispose();
  116. this._pollSpecs.dispose();
  117. Signal.clearData(this);
  118. }
  119. /**
  120. * Create an iterator over the most recent running sessions.
  121. *
  122. * @returns A new iterator over the running sessions.
  123. */
  124. running(): IIterator<Session.IModel> {
  125. return iter(this._models);
  126. }
  127. /**
  128. * Force a refresh of the specs from the server.
  129. *
  130. * @returns A promise that resolves when the specs are fetched.
  131. *
  132. * #### Notes
  133. * This is intended to be called only in response to a user action,
  134. * since the manager maintains its internal state.
  135. */
  136. async refreshSpecs(): Promise<void> {
  137. await this._pollSpecs.refresh();
  138. await this._pollSpecs.tick;
  139. }
  140. /**
  141. * Force a refresh of the running sessions.
  142. *
  143. * @returns A promise that with the list of running sessions.
  144. *
  145. * #### Notes
  146. * This is not typically meant to be called by the user, since the
  147. * manager maintains its own internal state.
  148. */
  149. async refreshRunning(): Promise<void> {
  150. await this._pollModels.refresh();
  151. await this._pollModels.tick;
  152. }
  153. /**
  154. * Start a new session. See also [[startNewSession]].
  155. *
  156. * @param options - Overrides for the default options, must include a `path`.
  157. */
  158. async startNew(options: Session.IOptions): Promise<Session.ISession> {
  159. const { serverSettings } = this;
  160. const session = await Session.startNew({ ...options, serverSettings });
  161. this._onStarted(session);
  162. return session;
  163. }
  164. /**
  165. * Find a session associated with a path and stop it if it is the only session
  166. * using that kernel.
  167. *
  168. * @param path - The path in question.
  169. *
  170. * @returns A promise that resolves when the relevant sessions are stopped.
  171. */
  172. async stopIfNeeded(path: string): Promise<void> {
  173. try {
  174. const sessions = await Session.listRunning(this.serverSettings);
  175. const matches = sessions.filter(value => value.path === path);
  176. if (matches.length === 1) {
  177. const id = matches[0].id;
  178. return this.shutdown(id).catch(() => {
  179. /* no-op */
  180. });
  181. }
  182. } catch (error) {
  183. /* Always succeed. */
  184. }
  185. }
  186. /**
  187. * Find a session by id.
  188. */
  189. findById(id: string): Promise<Session.IModel> {
  190. return Session.findById(id, this.serverSettings);
  191. }
  192. /**
  193. * Find a session by path.
  194. */
  195. findByPath(path: string): Promise<Session.IModel> {
  196. return Session.findByPath(path, this.serverSettings);
  197. }
  198. /*
  199. * Connect to a running session. See also [[connectToSession]].
  200. */
  201. connectTo(model: Session.IModel): Session.ISession {
  202. const session = Session.connectTo(model, this.serverSettings);
  203. this._onStarted(session);
  204. return session;
  205. }
  206. /**
  207. * Shut down a session by id.
  208. */
  209. async shutdown(id: string): Promise<void> {
  210. const models = this._models;
  211. const sessions = this._sessions;
  212. const index = ArrayExt.findFirstIndex(models, model => model.id === id);
  213. if (index === -1) {
  214. return;
  215. }
  216. // Proactively remove the model.
  217. models.splice(index, 1);
  218. this._runningChanged.emit(models.slice());
  219. sessions.forEach(session => {
  220. if (session.id === id) {
  221. sessions.delete(session);
  222. session.dispose();
  223. }
  224. });
  225. await Session.shutdown(id, this.serverSettings);
  226. }
  227. /**
  228. * Shut down all sessions.
  229. *
  230. * @returns A promise that resolves when all of the kernels are shut down.
  231. */
  232. async shutdownAll(): Promise<void> {
  233. // Update the list of models then shut down every session.
  234. try {
  235. await this.requestRunning();
  236. await Promise.all(
  237. this._models.map(({ id }) => Session.shutdown(id, this.serverSettings))
  238. );
  239. } finally {
  240. // Dispose every session and clear the set.
  241. this._sessions.forEach(kernel => {
  242. kernel.dispose();
  243. });
  244. this._sessions.clear();
  245. // Remove all models even if we had an error.
  246. if (this._models.length) {
  247. this._models.length = 0;
  248. this._runningChanged.emit([]);
  249. }
  250. }
  251. }
  252. /**
  253. * Execute a request to the server to poll running kernels and update state.
  254. */
  255. protected async requestRunning(): Promise<void> {
  256. const models = await Session.listRunning(this.serverSettings).catch(err => {
  257. // Check for a network error, or a 503 error, which is returned
  258. // by a JupyterHub when a server is shut down.
  259. if (
  260. err instanceof ServerConnection.NetworkError ||
  261. (err.response && err.response.status === 503)
  262. ) {
  263. this._connectionFailure.emit(err);
  264. return [] as Session.IModel[];
  265. }
  266. throw err;
  267. });
  268. if (this.isDisposed) {
  269. return;
  270. }
  271. if (!JSONExt.deepEqual(models, this._models)) {
  272. const ids = models.map(model => model.id);
  273. const sessions = this._sessions;
  274. sessions.forEach(session => {
  275. if (ids.indexOf(session.id) === -1) {
  276. session.dispose();
  277. sessions.delete(session);
  278. }
  279. });
  280. this._models = models.slice();
  281. this._runningChanged.emit(models);
  282. }
  283. }
  284. /**
  285. * Execute a request to the server to poll specs and update state.
  286. */
  287. protected async requestSpecs(): Promise<void> {
  288. const specs = await Kernel.getSpecs(this.serverSettings);
  289. if (this.isDisposed) {
  290. return;
  291. }
  292. if (!JSONExt.deepEqual(specs, this._specs)) {
  293. this._specs = specs;
  294. this._specsChanged.emit(specs);
  295. }
  296. }
  297. /**
  298. * Handle a session terminating.
  299. */
  300. private _onTerminated(id: string): void {
  301. // A session termination emission could mean the server session is deleted,
  302. // or that the session JS object is disposed and the session still exists on
  303. // the server, so we refresh from the server to make sure we reflect the
  304. // server state.
  305. this.refreshRunning().catch(e => {
  306. // Ignore poll rejection that arises if we are disposed
  307. // during this call.
  308. if (this.isDisposed) {
  309. return;
  310. }
  311. throw e;
  312. });
  313. }
  314. /**
  315. * Handle a session starting.
  316. */
  317. private _onStarted(session: Session.ISession): void {
  318. let id = session.id;
  319. let index = ArrayExt.findFirstIndex(this._models, value => value.id === id);
  320. this._sessions.add(session);
  321. if (index === -1) {
  322. this._models.push(session.model);
  323. this._runningChanged.emit(this._models.slice());
  324. }
  325. session.terminated.connect(s => {
  326. this._onTerminated(id);
  327. });
  328. session.propertyChanged.connect((sender, prop) => {
  329. this._onChanged(session.model);
  330. });
  331. session.kernelChanged.connect(() => {
  332. this._onChanged(session.model);
  333. });
  334. }
  335. /**
  336. * Handle a change to a session.
  337. */
  338. private _onChanged(model: Session.IModel): void {
  339. let index = ArrayExt.findFirstIndex(
  340. this._models,
  341. value => value.id === model.id
  342. );
  343. if (index !== -1) {
  344. this._models[index] = model;
  345. this._runningChanged.emit(this._models.slice());
  346. }
  347. }
  348. private _isDisposed = false;
  349. private _isReady = false;
  350. private _models: Session.IModel[] = [];
  351. private _pollModels: Poll;
  352. private _pollSpecs: Poll;
  353. private _ready: Promise<void>;
  354. private _runningChanged = new Signal<this, Session.IModel[]>(this);
  355. private _connectionFailure = new Signal<this, Error>(this);
  356. private _sessions = new Set<Session.ISession>();
  357. private _specs: Kernel.ISpecModels | null = null;
  358. private _specsChanged = new Signal<this, Kernel.ISpecModels>(this);
  359. }
  360. /**
  361. * The namespace for `SessionManager` class statics.
  362. */
  363. export namespace SessionManager {
  364. /**
  365. * The options used to initialize a SessionManager.
  366. */
  367. export interface IOptions {
  368. /**
  369. * The server settings for the manager.
  370. */
  371. serverSettings?: ServerConnection.ISettings;
  372. /**
  373. * When the manager stops polling the API. Defaults to `when-hidden`.
  374. */
  375. standby?: Poll.Standby;
  376. }
  377. }