@sasjs/adapter 4.9.2 → 4.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SessionManager.d.ts +1 -0
- package/api/viya/pollJobState.d.ts +21 -1
- package/index-dev.js +2 -2
- package/index.js +1 -1
- package/minified_sas9.js +1 -1
- package/node/SessionManager.d.ts +1 -0
- package/node/api/viya/pollJobState.d.ts +21 -1
- package/node/index.js +1 -1
- package/node/types/Session.d.ts +18 -1
- package/package.json +1 -1
- package/src/SessionManager.ts +27 -16
- package/src/api/viya/executeOnComputeApi.ts +12 -7
- package/src/api/viya/pollJobState.ts +59 -6
- package/src/api/viya/spec/mockResponses.ts +5 -3
- package/src/types/Session.ts +20 -1
- package/types/Session.d.ts +18 -1
package/node/types/Session.d.ts
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
import { Link } from './Link';
|
|
2
|
+
import { SessionManager } from '../SessionManager';
|
|
3
|
+
export declare enum SessionState {
|
|
4
|
+
Completed = "completed",
|
|
5
|
+
Running = "running",
|
|
6
|
+
Pending = "pending",
|
|
7
|
+
Idle = "idle",
|
|
8
|
+
Unavailable = "unavailable",
|
|
9
|
+
NoState = "",
|
|
10
|
+
Failed = "failed",
|
|
11
|
+
Error = "error"
|
|
12
|
+
}
|
|
2
13
|
export interface Session {
|
|
3
14
|
id: string;
|
|
4
|
-
state:
|
|
15
|
+
state: SessionState;
|
|
16
|
+
stateUrl: string;
|
|
5
17
|
links: Link[];
|
|
6
18
|
attributes: {
|
|
7
19
|
sessionInactiveTimeout: number;
|
|
8
20
|
};
|
|
9
21
|
creationTimeStamp: string;
|
|
22
|
+
etag: string;
|
|
10
23
|
}
|
|
11
24
|
export interface SessionVariable {
|
|
12
25
|
value: string;
|
|
13
26
|
}
|
|
27
|
+
export interface JobSessionManager {
|
|
28
|
+
session: Session;
|
|
29
|
+
sessionManager: SessionManager;
|
|
30
|
+
}
|
package/package.json
CHANGED
package/src/SessionManager.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Session, Context, SessionVariable } from './types'
|
|
1
|
+
import { Session, Context, SessionVariable, SessionState } from './types'
|
|
2
2
|
import { NoSessionStateError } from './types/errors'
|
|
3
3
|
import { asyncForEach, isUrl } from './utils'
|
|
4
4
|
import { prefixMessage } from '@sasjs/utils/error'
|
|
@@ -12,6 +12,7 @@ interface ApiErrorResponse {
|
|
|
12
12
|
|
|
13
13
|
export class SessionManager {
|
|
14
14
|
private loggedErrors: NoSessionStateError[] = []
|
|
15
|
+
private sessionStateLinkError = 'Error while getting session state link. '
|
|
15
16
|
|
|
16
17
|
constructor(
|
|
17
18
|
private serverUrl: string,
|
|
@@ -28,7 +29,7 @@ export class SessionManager {
|
|
|
28
29
|
private _debug: boolean = false
|
|
29
30
|
private printedSessionState = {
|
|
30
31
|
printed: false,
|
|
31
|
-
state:
|
|
32
|
+
state: SessionState.NoState
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
public get debug() {
|
|
@@ -265,6 +266,18 @@ export class SessionManager {
|
|
|
265
266
|
)
|
|
266
267
|
})
|
|
267
268
|
|
|
269
|
+
// Add response etag to Session object.
|
|
270
|
+
createdSession.etag = etag
|
|
271
|
+
|
|
272
|
+
// Get session state link.
|
|
273
|
+
const stateLink = createdSession.links.find((link) => link.rel === 'state')
|
|
274
|
+
|
|
275
|
+
// Throw error if session state link is not present.
|
|
276
|
+
if (!stateLink) throw this.sessionStateLinkError
|
|
277
|
+
|
|
278
|
+
// Add session state link to Session object.
|
|
279
|
+
createdSession.stateUrl = stateLink.href
|
|
280
|
+
|
|
268
281
|
await this.waitForSession(createdSession, etag, accessToken)
|
|
269
282
|
|
|
270
283
|
this.sessions.push(createdSession)
|
|
@@ -327,32 +340,30 @@ export class SessionManager {
|
|
|
327
340
|
etag: string | null,
|
|
328
341
|
accessToken?: string
|
|
329
342
|
): Promise<string> {
|
|
343
|
+
let { state: sessionState } = session
|
|
344
|
+
const { stateUrl } = session
|
|
330
345
|
const logger = process.logger || console
|
|
331
346
|
|
|
332
|
-
let sessionState = session.state
|
|
333
|
-
|
|
334
|
-
const stateLink = session.links.find((l: any) => l.rel === 'state')
|
|
335
|
-
|
|
336
347
|
if (
|
|
337
|
-
sessionState ===
|
|
338
|
-
sessionState ===
|
|
339
|
-
sessionState ===
|
|
348
|
+
sessionState === SessionState.Pending ||
|
|
349
|
+
sessionState === SessionState.Running ||
|
|
350
|
+
sessionState === SessionState.NoState
|
|
340
351
|
) {
|
|
341
|
-
if (
|
|
352
|
+
if (stateUrl) {
|
|
342
353
|
if (this.debug && !this.printedSessionState.printed) {
|
|
343
|
-
logger.info(`Polling: ${this.serverUrl +
|
|
354
|
+
logger.info(`Polling: ${this.serverUrl + stateUrl}`)
|
|
344
355
|
|
|
345
356
|
this.printedSessionState.printed = true
|
|
346
357
|
}
|
|
347
358
|
|
|
348
|
-
const url = `${this.serverUrl}${
|
|
359
|
+
const url = `${this.serverUrl}${stateUrl}?wait=30`
|
|
349
360
|
|
|
350
361
|
const { result: state, responseStatus: responseStatus } =
|
|
351
362
|
await this.getSessionState(url, etag!, accessToken).catch((err) => {
|
|
352
363
|
throw prefixMessage(err, 'Error while waiting for session. ')
|
|
353
364
|
})
|
|
354
365
|
|
|
355
|
-
sessionState = state.trim()
|
|
366
|
+
sessionState = state.trim() as SessionState
|
|
356
367
|
|
|
357
368
|
if (this.debug && this.printedSessionState.state !== sessionState) {
|
|
358
369
|
logger.info(`Current session state is '${sessionState}'`)
|
|
@@ -364,7 +375,7 @@ export class SessionManager {
|
|
|
364
375
|
if (!sessionState) {
|
|
365
376
|
const stateError = new NoSessionStateError(
|
|
366
377
|
responseStatus,
|
|
367
|
-
this.serverUrl +
|
|
378
|
+
this.serverUrl + stateUrl,
|
|
368
379
|
session.links.find((l: any) => l.rel === 'log')?.href as string
|
|
369
380
|
)
|
|
370
381
|
|
|
@@ -386,7 +397,7 @@ export class SessionManager {
|
|
|
386
397
|
|
|
387
398
|
return sessionState
|
|
388
399
|
} else {
|
|
389
|
-
throw
|
|
400
|
+
throw this.sessionStateLinkError
|
|
390
401
|
}
|
|
391
402
|
} else {
|
|
392
403
|
this.loggedErrors = []
|
|
@@ -413,7 +424,7 @@ export class SessionManager {
|
|
|
413
424
|
return await this.requestClient
|
|
414
425
|
.get(url, accessToken, 'text/plain', { 'If-None-Match': etag })
|
|
415
426
|
.then((res) => ({
|
|
416
|
-
result: res.result as
|
|
427
|
+
result: res.result as SessionState,
|
|
417
428
|
responseStatus: res.status
|
|
418
429
|
}))
|
|
419
430
|
.catch((err) => {
|
|
@@ -170,16 +170,21 @@ export async function executeOnComputeApi(
|
|
|
170
170
|
postedJob,
|
|
171
171
|
debug,
|
|
172
172
|
authConfig,
|
|
173
|
-
pollOptions
|
|
173
|
+
pollOptions,
|
|
174
|
+
{
|
|
175
|
+
session,
|
|
176
|
+
sessionManager
|
|
177
|
+
}
|
|
174
178
|
).catch(async (err) => {
|
|
175
179
|
const error = err?.response?.data
|
|
176
180
|
const result = /err=[0-9]*,/.exec(error)
|
|
177
|
-
|
|
178
181
|
const errorCode = '5113'
|
|
182
|
+
|
|
179
183
|
if (result?.[0]?.slice(4, -1) === errorCode) {
|
|
184
|
+
const logCount = 1000000
|
|
180
185
|
const sessionLogUrl =
|
|
181
186
|
postedJob.links.find((l: any) => l.rel === 'up')!.href + '/log'
|
|
182
|
-
|
|
187
|
+
|
|
183
188
|
err.log = await fetchLogByChunks(
|
|
184
189
|
requestClient,
|
|
185
190
|
access_token!,
|
|
@@ -187,6 +192,7 @@ export async function executeOnComputeApi(
|
|
|
187
192
|
logCount
|
|
188
193
|
)
|
|
189
194
|
}
|
|
195
|
+
|
|
190
196
|
throw prefixMessage(err, 'Error while polling job status. ')
|
|
191
197
|
})
|
|
192
198
|
|
|
@@ -205,12 +211,12 @@ export async function executeOnComputeApi(
|
|
|
205
211
|
|
|
206
212
|
let jobResult
|
|
207
213
|
let log = ''
|
|
208
|
-
|
|
209
214
|
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
|
210
215
|
|
|
211
216
|
if (debug && logLink) {
|
|
212
217
|
const logUrl = `${logLink.href}/content`
|
|
213
218
|
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
|
219
|
+
|
|
214
220
|
log = await fetchLogByChunks(
|
|
215
221
|
requestClient,
|
|
216
222
|
access_token!,
|
|
@@ -223,9 +229,7 @@ export async function executeOnComputeApi(
|
|
|
223
229
|
throw new ComputeJobExecutionError(currentJob, log)
|
|
224
230
|
}
|
|
225
231
|
|
|
226
|
-
if (!expectWebout) {
|
|
227
|
-
return { job: currentJob, log }
|
|
228
|
-
}
|
|
232
|
+
if (!expectWebout) return { job: currentJob, log }
|
|
229
233
|
|
|
230
234
|
const resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
|
|
231
235
|
|
|
@@ -236,6 +240,7 @@ export async function executeOnComputeApi(
|
|
|
236
240
|
if (logLink) {
|
|
237
241
|
const logUrl = `${logLink.href}/content`
|
|
238
242
|
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
|
243
|
+
|
|
239
244
|
log = await fetchLogByChunks(
|
|
240
245
|
requestClient,
|
|
241
246
|
access_token!,
|
|
@@ -3,7 +3,7 @@ import { Job, PollOptions, PollStrategy } from '../..'
|
|
|
3
3
|
import { getTokens } from '../../auth/getTokens'
|
|
4
4
|
import { RequestClient } from '../../request/RequestClient'
|
|
5
5
|
import { JobStatePollError } from '../../types/errors'
|
|
6
|
-
import { Link, WriteStream } from '../../types'
|
|
6
|
+
import { Link, WriteStream, SessionState, JobSessionManager } from '../../types'
|
|
7
7
|
import { delay, isNode } from '../../utils'
|
|
8
8
|
|
|
9
9
|
export enum JobState {
|
|
@@ -37,6 +37,7 @@ export enum JobState {
|
|
|
37
37
|
* { maxPollCount: 500, pollInterval: 30000 }, // approximately ~50.5 mins (including time to get response (~300ms))
|
|
38
38
|
* { maxPollCount: 3400, pollInterval: 60000 } // approximately ~3015 mins (~125 hours) (including time to get response (~300ms))
|
|
39
39
|
* ]
|
|
40
|
+
* @param jobSessionManager - job session object containing session object and an instance of Session Manager. Job session object is used to periodically (every 10th job state poll) check parent session state.
|
|
40
41
|
* @returns - a promise which resolves with a job state
|
|
41
42
|
*/
|
|
42
43
|
export async function pollJobState(
|
|
@@ -44,7 +45,8 @@ export async function pollJobState(
|
|
|
44
45
|
postedJob: Job,
|
|
45
46
|
debug: boolean,
|
|
46
47
|
authConfig?: AuthConfig,
|
|
47
|
-
pollOptions?: PollOptions
|
|
48
|
+
pollOptions?: PollOptions,
|
|
49
|
+
jobSessionManager?: JobSessionManager
|
|
48
50
|
): Promise<JobState> {
|
|
49
51
|
const logger = process.logger || console
|
|
50
52
|
|
|
@@ -127,7 +129,8 @@ export async function pollJobState(
|
|
|
127
129
|
pollOptions,
|
|
128
130
|
authConfig,
|
|
129
131
|
streamLog,
|
|
130
|
-
logFileStream
|
|
132
|
+
logFileStream,
|
|
133
|
+
jobSessionManager
|
|
131
134
|
)
|
|
132
135
|
|
|
133
136
|
currentState = result.state
|
|
@@ -158,7 +161,8 @@ export async function pollJobState(
|
|
|
158
161
|
defaultPollOptions,
|
|
159
162
|
authConfig,
|
|
160
163
|
streamLog,
|
|
161
|
-
logFileStream
|
|
164
|
+
logFileStream,
|
|
165
|
+
jobSessionManager
|
|
162
166
|
)
|
|
163
167
|
|
|
164
168
|
currentState = result.state
|
|
@@ -208,7 +212,21 @@ const needsRetry = (state: string) =>
|
|
|
208
212
|
state === JobState.Pending ||
|
|
209
213
|
state === JobState.Unavailable
|
|
210
214
|
|
|
211
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Polls job state.
|
|
217
|
+
* @param requestClient - the pre-configured HTTP request client.
|
|
218
|
+
* @param postedJob - the relative or absolute path to the job.
|
|
219
|
+
* @param currentState - current job state.
|
|
220
|
+
* @param debug - sets the _debug flag in the job arguments.
|
|
221
|
+
* @param pollCount - current poll count.
|
|
222
|
+
* @param pollOptions - an object containing maxPollCount, pollInterval, streamLog and logFolderPath.
|
|
223
|
+
* @param authConfig - an access token, refresh token, client and secret for an authorized user.
|
|
224
|
+
* @param streamLog - indicates if job log should be streamed.
|
|
225
|
+
* @param logStream - job log stream.
|
|
226
|
+
* @param jobSessionManager - job session object containing session object and an instance of Session Manager. Job session object is used to periodically (every 10th job state poll) check parent session state. Session state is considered healthy if it is equal to 'running' or 'idle'.
|
|
227
|
+
* @returns - a promise which resolves with a job state
|
|
228
|
+
*/
|
|
229
|
+
export const doPoll = async (
|
|
212
230
|
requestClient: RequestClient,
|
|
213
231
|
postedJob: Job,
|
|
214
232
|
currentState: JobState,
|
|
@@ -217,7 +235,8 @@ const doPoll = async (
|
|
|
217
235
|
pollOptions: PollOptions,
|
|
218
236
|
authConfig?: AuthConfig,
|
|
219
237
|
streamLog?: boolean,
|
|
220
|
-
logStream?: WriteStream
|
|
238
|
+
logStream?: WriteStream,
|
|
239
|
+
jobSessionManager?: JobSessionManager
|
|
221
240
|
): Promise<{ state: JobState; pollCount: number }> => {
|
|
222
241
|
const { maxPollCount, pollInterval } = pollOptions
|
|
223
242
|
const logger = process.logger || console
|
|
@@ -229,6 +248,40 @@ const doPoll = async (
|
|
|
229
248
|
let startLogLine = 0
|
|
230
249
|
|
|
231
250
|
while (needsRetry(state) && pollCount <= maxPollCount) {
|
|
251
|
+
// Check parent session state on every 10th job state poll.
|
|
252
|
+
if (jobSessionManager && pollCount && pollCount % 10 === 0 && authConfig) {
|
|
253
|
+
const { session, sessionManager } = jobSessionManager
|
|
254
|
+
const { stateUrl, etag, id: sessionId } = session
|
|
255
|
+
const { access_token } = authConfig
|
|
256
|
+
const { id: jobId } = postedJob
|
|
257
|
+
|
|
258
|
+
// Get session state.
|
|
259
|
+
const { result: sessionState, responseStatus } = await sessionManager[
|
|
260
|
+
'getSessionState'
|
|
261
|
+
](stateUrl, etag, access_token).catch((err) => {
|
|
262
|
+
// Handle error while getting session state.
|
|
263
|
+
throw new JobStatePollError(jobId, err)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
// Checks if session state is equal to 'running' or 'idle'.
|
|
267
|
+
const isSessionStatesHealthy = (state: string) =>
|
|
268
|
+
[SessionState.Running, SessionState.Idle].includes(
|
|
269
|
+
state as SessionState
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
// Clear parent session and throw an error if session state is not
|
|
273
|
+
// 'running', 'idle' or response status is not 200.
|
|
274
|
+
if (!isSessionStatesHealthy(sessionState) || responseStatus !== 200) {
|
|
275
|
+
sessionManager.clearSession(sessionId, access_token)
|
|
276
|
+
|
|
277
|
+
const sessionError = isSessionStatesHealthy(sessionState)
|
|
278
|
+
? `Session response status is not 200. Session response status is ${responseStatus}.`
|
|
279
|
+
: `Session state of the job is not 'running' or 'idle'. Session state is '${sessionState}'`
|
|
280
|
+
|
|
281
|
+
throw new JobStatePollError(jobId, new Error(sessionError))
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
232
285
|
state = await getJobState(
|
|
233
286
|
requestClient,
|
|
234
287
|
postedJob,
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { AuthConfig } from '@sasjs/utils/types'
|
|
2
|
-
import { Job, Session } from '../../../types'
|
|
2
|
+
import { Job, Session, SessionState } from '../../../types'
|
|
3
3
|
|
|
4
4
|
export const mockSession: Session = {
|
|
5
5
|
id: 's35510n',
|
|
6
|
-
state:
|
|
6
|
+
state: SessionState.Idle,
|
|
7
|
+
stateUrl: '',
|
|
7
8
|
links: [],
|
|
8
9
|
attributes: {
|
|
9
10
|
sessionInactiveTimeout: 1
|
|
10
11
|
},
|
|
11
|
-
creationTimeStamp: new Date().valueOf().toString()
|
|
12
|
+
creationTimeStamp: new Date().valueOf().toString(),
|
|
13
|
+
etag: 'etag-string'
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export const mockJob: Job = {
|
package/src/types/Session.ts
CHANGED
|
@@ -1,15 +1,34 @@
|
|
|
1
1
|
import { Link } from './Link'
|
|
2
|
+
import { SessionManager } from '../SessionManager'
|
|
3
|
+
|
|
4
|
+
export enum SessionState {
|
|
5
|
+
Completed = 'completed',
|
|
6
|
+
Running = 'running',
|
|
7
|
+
Pending = 'pending',
|
|
8
|
+
Idle = 'idle',
|
|
9
|
+
Unavailable = 'unavailable',
|
|
10
|
+
NoState = '',
|
|
11
|
+
Failed = 'failed',
|
|
12
|
+
Error = 'error'
|
|
13
|
+
}
|
|
2
14
|
|
|
3
15
|
export interface Session {
|
|
4
16
|
id: string
|
|
5
|
-
state:
|
|
17
|
+
state: SessionState
|
|
18
|
+
stateUrl: string
|
|
6
19
|
links: Link[]
|
|
7
20
|
attributes: {
|
|
8
21
|
sessionInactiveTimeout: number
|
|
9
22
|
}
|
|
10
23
|
creationTimeStamp: string
|
|
24
|
+
etag: string
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
export interface SessionVariable {
|
|
14
28
|
value: string
|
|
15
29
|
}
|
|
30
|
+
|
|
31
|
+
export interface JobSessionManager {
|
|
32
|
+
session: Session
|
|
33
|
+
sessionManager: SessionManager
|
|
34
|
+
}
|
package/types/Session.d.ts
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
import { Link } from './Link';
|
|
2
|
+
import { SessionManager } from '../SessionManager';
|
|
3
|
+
export declare enum SessionState {
|
|
4
|
+
Completed = "completed",
|
|
5
|
+
Running = "running",
|
|
6
|
+
Pending = "pending",
|
|
7
|
+
Idle = "idle",
|
|
8
|
+
Unavailable = "unavailable",
|
|
9
|
+
NoState = "",
|
|
10
|
+
Failed = "failed",
|
|
11
|
+
Error = "error"
|
|
12
|
+
}
|
|
2
13
|
export interface Session {
|
|
3
14
|
id: string;
|
|
4
|
-
state:
|
|
15
|
+
state: SessionState;
|
|
16
|
+
stateUrl: string;
|
|
5
17
|
links: Link[];
|
|
6
18
|
attributes: {
|
|
7
19
|
sessionInactiveTimeout: number;
|
|
8
20
|
};
|
|
9
21
|
creationTimeStamp: string;
|
|
22
|
+
etag: string;
|
|
10
23
|
}
|
|
11
24
|
export interface SessionVariable {
|
|
12
25
|
value: string;
|
|
13
26
|
}
|
|
27
|
+
export interface JobSessionManager {
|
|
28
|
+
session: Session;
|
|
29
|
+
sessionManager: SessionManager;
|
|
30
|
+
}
|