diff options
author | Chocobozzz <me@florianbigard.com> | 2023-07-31 14:34:36 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-08-11 15:02:33 +0200 |
commit | 3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch) | |
tree | e4510b39bdac9c318fdb4b47018d08f15368b8f0 /packages/tests/src/api/runners/runner-common.ts | |
parent | 04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff) | |
download | PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip |
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge
conflicts, but it's a major step forward:
* Server can be faster at startup because imports() are async and we can
easily lazy import big modules
* Angular doesn't seem to support ES import (with .js extension), so we
had to correctly organize peertube into a monorepo:
* Use yarn workspace feature
* Use typescript reference projects for dependencies
* Shared projects have been moved into "packages", each one is now a
node module (with a dedicated package.json/tsconfig.json)
* server/tools have been moved into apps/ and is now a dedicated app
bundled and published on NPM so users don't have to build peertube
cli tools manually
* server/tests have been moved into packages/ so we don't compile
them every time we want to run the server
* Use isolatedModule option:
* Had to move from const enum to const
(https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums)
* Had to explictely specify "type" imports when used in decorators
* Prefer tsx (that uses esbuild under the hood) instead of ts-node to
load typescript files (tests with mocha or scripts):
* To reduce test complexity as esbuild doesn't support decorator
metadata, we only test server files that do not import server
models
* We still build tests files into js files for a faster CI
* Remove unmaintained peertube CLI import script
* Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'packages/tests/src/api/runners/runner-common.ts')
-rw-r--r-- | packages/tests/src/api/runners/runner-common.ts | 744 |
1 files changed, 744 insertions, 0 deletions
diff --git a/packages/tests/src/api/runners/runner-common.ts b/packages/tests/src/api/runners/runner-common.ts new file mode 100644 index 000000000..53ea321d0 --- /dev/null +++ b/packages/tests/src/api/runners/runner-common.ts | |||
@@ -0,0 +1,744 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { wait } from '@peertube/peertube-core-utils' | ||
4 | import { | ||
5 | HttpStatusCode, | ||
6 | Runner, | ||
7 | RunnerJob, | ||
8 | RunnerJobAdmin, | ||
9 | RunnerJobState, | ||
10 | RunnerJobStateType, | ||
11 | RunnerJobVODWebVideoTranscodingPayload, | ||
12 | RunnerRegistrationToken | ||
13 | } from '@peertube/peertube-models' | ||
14 | import { | ||
15 | PeerTubeServer, | ||
16 | cleanupTests, | ||
17 | createSingleServer, | ||
18 | setAccessTokensToServers, | ||
19 | setDefaultVideoChannel, | ||
20 | waitJobs | ||
21 | } from '@peertube/peertube-server-commands' | ||
22 | import { expect } from 'chai' | ||
23 | |||
24 | describe('Test runner common actions', function () { | ||
25 | let server: PeerTubeServer | ||
26 | let registrationToken: string | ||
27 | let runnerToken: string | ||
28 | let jobMaxPriority: string | ||
29 | |||
30 | before(async function () { | ||
31 | this.timeout(120_000) | ||
32 | |||
33 | server = await createSingleServer(1, { | ||
34 | remote_runners: { | ||
35 | stalled_jobs: { | ||
36 | vod: '5 seconds' | ||
37 | } | ||
38 | } | ||
39 | }) | ||
40 | |||
41 | await setAccessTokensToServers([ server ]) | ||
42 | await setDefaultVideoChannel([ server ]) | ||
43 | |||
44 | await server.config.enableTranscoding({ hls: true, webVideo: true }) | ||
45 | await server.config.enableRemoteTranscoding() | ||
46 | }) | ||
47 | |||
48 | describe('Managing runner registration tokens', function () { | ||
49 | let base: RunnerRegistrationToken[] | ||
50 | let registrationTokenToDelete: RunnerRegistrationToken | ||
51 | |||
52 | it('Should have a default registration token', async function () { | ||
53 | const { total, data } = await server.runnerRegistrationTokens.list() | ||
54 | |||
55 | expect(total).to.equal(1) | ||
56 | expect(data).to.have.lengthOf(1) | ||
57 | |||
58 | const token = data[0] | ||
59 | expect(token.id).to.exist | ||
60 | expect(token.createdAt).to.exist | ||
61 | expect(token.updatedAt).to.exist | ||
62 | expect(token.registeredRunnersCount).to.equal(0) | ||
63 | expect(token.registrationToken).to.exist | ||
64 | }) | ||
65 | |||
66 | it('Should create other registration tokens', async function () { | ||
67 | await server.runnerRegistrationTokens.generate() | ||
68 | await server.runnerRegistrationTokens.generate() | ||
69 | |||
70 | const { total, data } = await server.runnerRegistrationTokens.list() | ||
71 | expect(total).to.equal(3) | ||
72 | expect(data).to.have.lengthOf(3) | ||
73 | }) | ||
74 | |||
75 | it('Should list registration tokens', async function () { | ||
76 | { | ||
77 | const { total, data } = await server.runnerRegistrationTokens.list({ sort: 'createdAt' }) | ||
78 | expect(total).to.equal(3) | ||
79 | expect(data).to.have.lengthOf(3) | ||
80 | expect(new Date(data[0].createdAt)).to.be.below(new Date(data[1].createdAt)) | ||
81 | expect(new Date(data[1].createdAt)).to.be.below(new Date(data[2].createdAt)) | ||
82 | |||
83 | base = data | ||
84 | |||
85 | registrationTokenToDelete = data[0] | ||
86 | registrationToken = data[1].registrationToken | ||
87 | } | ||
88 | |||
89 | { | ||
90 | const { total, data } = await server.runnerRegistrationTokens.list({ sort: '-createdAt', start: 2, count: 1 }) | ||
91 | expect(total).to.equal(3) | ||
92 | expect(data).to.have.lengthOf(1) | ||
93 | expect(data[0].registrationToken).to.equal(base[0].registrationToken) | ||
94 | } | ||
95 | }) | ||
96 | |||
97 | it('Should have appropriate registeredRunnersCount for registration tokens', async function () { | ||
98 | await server.runners.register({ name: 'to delete 1', registrationToken: registrationTokenToDelete.registrationToken }) | ||
99 | await server.runners.register({ name: 'to delete 2', registrationToken: registrationTokenToDelete.registrationToken }) | ||
100 | |||
101 | const { data } = await server.runnerRegistrationTokens.list() | ||
102 | |||
103 | for (const d of data) { | ||
104 | if (d.registrationToken === registrationTokenToDelete.registrationToken) { | ||
105 | expect(d.registeredRunnersCount).to.equal(2) | ||
106 | } else { | ||
107 | expect(d.registeredRunnersCount).to.equal(0) | ||
108 | } | ||
109 | } | ||
110 | |||
111 | const { data: runners } = await server.runners.list() | ||
112 | expect(runners).to.have.lengthOf(2) | ||
113 | }) | ||
114 | |||
115 | it('Should delete a registration token', async function () { | ||
116 | await server.runnerRegistrationTokens.delete({ id: registrationTokenToDelete.id }) | ||
117 | |||
118 | const { total, data } = await server.runnerRegistrationTokens.list({ sort: 'createdAt' }) | ||
119 | expect(total).to.equal(2) | ||
120 | expect(data).to.have.lengthOf(2) | ||
121 | |||
122 | for (const d of data) { | ||
123 | expect(d.registeredRunnersCount).to.equal(0) | ||
124 | expect(d.registrationToken).to.not.equal(registrationTokenToDelete.registrationToken) | ||
125 | } | ||
126 | }) | ||
127 | |||
128 | it('Should have removed runners of this registration token', async function () { | ||
129 | const { data: runners } = await server.runners.list() | ||
130 | expect(runners).to.have.lengthOf(0) | ||
131 | }) | ||
132 | }) | ||
133 | |||
134 | describe('Managing runners', function () { | ||
135 | let toDelete: Runner | ||
136 | |||
137 | it('Should not have runners available', async function () { | ||
138 | const { total, data } = await server.runners.list() | ||
139 | |||
140 | expect(data).to.have.lengthOf(0) | ||
141 | expect(total).to.equal(0) | ||
142 | }) | ||
143 | |||
144 | it('Should register runners', async function () { | ||
145 | const now = new Date() | ||
146 | |||
147 | const result = await server.runners.register({ | ||
148 | name: 'runner 1', | ||
149 | description: 'my super runner 1', | ||
150 | registrationToken | ||
151 | }) | ||
152 | expect(result.runnerToken).to.exist | ||
153 | runnerToken = result.runnerToken | ||
154 | |||
155 | await server.runners.register({ | ||
156 | name: 'runner 2', | ||
157 | registrationToken | ||
158 | }) | ||
159 | |||
160 | const { total, data } = await server.runners.list({ sort: 'createdAt' }) | ||
161 | expect(total).to.equal(2) | ||
162 | expect(data).to.have.lengthOf(2) | ||
163 | |||
164 | for (const d of data) { | ||
165 | expect(d.id).to.exist | ||
166 | expect(d.createdAt).to.exist | ||
167 | expect(d.updatedAt).to.exist | ||
168 | expect(new Date(d.createdAt)).to.be.above(now) | ||
169 | expect(new Date(d.updatedAt)).to.be.above(now) | ||
170 | expect(new Date(d.lastContact)).to.be.above(now) | ||
171 | expect(d.ip).to.exist | ||
172 | } | ||
173 | |||
174 | expect(data[0].name).to.equal('runner 1') | ||
175 | expect(data[0].description).to.equal('my super runner 1') | ||
176 | |||
177 | expect(data[1].name).to.equal('runner 2') | ||
178 | expect(data[1].description).to.be.null | ||
179 | |||
180 | toDelete = data[1] | ||
181 | }) | ||
182 | |||
183 | it('Should list runners', async function () { | ||
184 | const { total, data } = await server.runners.list({ sort: '-createdAt', start: 1, count: 1 }) | ||
185 | |||
186 | expect(total).to.equal(2) | ||
187 | expect(data).to.have.lengthOf(1) | ||
188 | expect(data[0].name).to.equal('runner 1') | ||
189 | }) | ||
190 | |||
191 | it('Should delete a runner', async function () { | ||
192 | await server.runners.delete({ id: toDelete.id }) | ||
193 | |||
194 | const { total, data } = await server.runners.list() | ||
195 | |||
196 | expect(total).to.equal(1) | ||
197 | expect(data).to.have.lengthOf(1) | ||
198 | expect(data[0].name).to.equal('runner 1') | ||
199 | }) | ||
200 | |||
201 | it('Should unregister a runner', async function () { | ||
202 | const registered = await server.runners.autoRegisterRunner() | ||
203 | |||
204 | { | ||
205 | const { total, data } = await server.runners.list() | ||
206 | expect(total).to.equal(2) | ||
207 | expect(data).to.have.lengthOf(2) | ||
208 | } | ||
209 | |||
210 | await server.runners.unregister({ runnerToken: registered }) | ||
211 | |||
212 | { | ||
213 | const { total, data } = await server.runners.list() | ||
214 | expect(total).to.equal(1) | ||
215 | expect(data).to.have.lengthOf(1) | ||
216 | expect(data[0].name).to.equal('runner 1') | ||
217 | } | ||
218 | }) | ||
219 | }) | ||
220 | |||
221 | describe('Managing runner jobs', function () { | ||
222 | let jobUUID: string | ||
223 | let jobToken: string | ||
224 | let lastRunnerContact: Date | ||
225 | let failedJob: RunnerJob | ||
226 | |||
227 | async function checkMainJobState ( | ||
228 | mainJobState: RunnerJobStateType, | ||
229 | otherJobStates: RunnerJobStateType[] = [ RunnerJobState.PENDING, RunnerJobState.WAITING_FOR_PARENT_JOB ] | ||
230 | ) { | ||
231 | const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' }) | ||
232 | |||
233 | for (const job of data) { | ||
234 | if (job.uuid === jobUUID) { | ||
235 | expect(job.state.id).to.equal(mainJobState) | ||
236 | } else { | ||
237 | expect(otherJobStates).to.include(job.state.id) | ||
238 | } | ||
239 | } | ||
240 | } | ||
241 | |||
242 | function getMainJob () { | ||
243 | return server.runnerJobs.getJob({ uuid: jobUUID }) | ||
244 | } | ||
245 | |||
246 | describe('List jobs', function () { | ||
247 | |||
248 | it('Should not have jobs', async function () { | ||
249 | const { total, data } = await server.runnerJobs.list() | ||
250 | |||
251 | expect(data).to.have.lengthOf(0) | ||
252 | expect(total).to.equal(0) | ||
253 | }) | ||
254 | |||
255 | it('Should upload a video and have available jobs', async function () { | ||
256 | await server.videos.quickUpload({ name: 'to transcode' }) | ||
257 | await waitJobs([ server ]) | ||
258 | |||
259 | const { total, data } = await server.runnerJobs.list() | ||
260 | |||
261 | expect(data).to.have.lengthOf(10) | ||
262 | expect(total).to.equal(10) | ||
263 | |||
264 | for (const job of data) { | ||
265 | expect(job.startedAt).to.not.exist | ||
266 | expect(job.finishedAt).to.not.exist | ||
267 | expect(job.payload).to.exist | ||
268 | expect(job.privatePayload).to.exist | ||
269 | } | ||
270 | |||
271 | const hlsJobs = data.filter(d => d.type === 'vod-hls-transcoding') | ||
272 | const webVideoJobs = data.filter(d => d.type === 'vod-web-video-transcoding') | ||
273 | |||
274 | expect(hlsJobs).to.have.lengthOf(5) | ||
275 | expect(webVideoJobs).to.have.lengthOf(5) | ||
276 | |||
277 | const pendingJobs = data.filter(d => d.state.id === RunnerJobState.PENDING) | ||
278 | const waitingJobs = data.filter(d => d.state.id === RunnerJobState.WAITING_FOR_PARENT_JOB) | ||
279 | |||
280 | expect(pendingJobs).to.have.lengthOf(1) | ||
281 | expect(waitingJobs).to.have.lengthOf(9) | ||
282 | }) | ||
283 | |||
284 | it('Should upload another video and list/sort jobs', async function () { | ||
285 | await server.videos.quickUpload({ name: 'to transcode 2' }) | ||
286 | await waitJobs([ server ]) | ||
287 | |||
288 | { | ||
289 | const { total, data } = await server.runnerJobs.list({ start: 0, count: 30 }) | ||
290 | |||
291 | expect(data).to.have.lengthOf(20) | ||
292 | expect(total).to.equal(20) | ||
293 | |||
294 | jobUUID = data[16].uuid | ||
295 | } | ||
296 | |||
297 | { | ||
298 | const { total, data } = await server.runnerJobs.list({ start: 3, count: 1, sort: 'createdAt' }) | ||
299 | expect(total).to.equal(20) | ||
300 | |||
301 | expect(data).to.have.lengthOf(1) | ||
302 | expect(data[0].uuid).to.equal(jobUUID) | ||
303 | } | ||
304 | |||
305 | { | ||
306 | let previousPriority = Infinity | ||
307 | const { total, data } = await server.runnerJobs.list({ start: 0, count: 100, sort: '-priority' }) | ||
308 | expect(total).to.equal(20) | ||
309 | |||
310 | for (const job of data) { | ||
311 | expect(job.priority).to.be.at.most(previousPriority) | ||
312 | previousPriority = job.priority | ||
313 | |||
314 | if (job.state.id === RunnerJobState.PENDING) { | ||
315 | jobMaxPriority = job.uuid | ||
316 | } | ||
317 | } | ||
318 | } | ||
319 | }) | ||
320 | |||
321 | it('Should search jobs', async function () { | ||
322 | { | ||
323 | const { total, data } = await server.runnerJobs.list({ search: jobUUID }) | ||
324 | |||
325 | expect(data).to.have.lengthOf(1) | ||
326 | expect(total).to.equal(1) | ||
327 | |||
328 | expect(data[0].uuid).to.equal(jobUUID) | ||
329 | } | ||
330 | |||
331 | { | ||
332 | const { total, data } = await server.runnerJobs.list({ search: 'toto' }) | ||
333 | |||
334 | expect(data).to.have.lengthOf(0) | ||
335 | expect(total).to.equal(0) | ||
336 | } | ||
337 | |||
338 | { | ||
339 | const { total, data } = await server.runnerJobs.list({ search: 'hls' }) | ||
340 | |||
341 | expect(data).to.not.have.lengthOf(0) | ||
342 | expect(total).to.not.equal(0) | ||
343 | |||
344 | for (const job of data) { | ||
345 | expect(job.type).to.include('hls') | ||
346 | } | ||
347 | } | ||
348 | }) | ||
349 | |||
350 | it('Should filter jobs', async function () { | ||
351 | { | ||
352 | const { total, data } = await server.runnerJobs.list({ stateOneOf: [ RunnerJobState.WAITING_FOR_PARENT_JOB ] }) | ||
353 | |||
354 | expect(data).to.not.have.lengthOf(0) | ||
355 | expect(total).to.not.equal(0) | ||
356 | |||
357 | for (const job of data) { | ||
358 | expect(job.state.label).to.equal('Waiting for parent job to finish') | ||
359 | } | ||
360 | } | ||
361 | |||
362 | { | ||
363 | const { total, data } = await server.runnerJobs.list({ stateOneOf: [ RunnerJobState.COMPLETED ] }) | ||
364 | |||
365 | expect(data).to.have.lengthOf(0) | ||
366 | expect(total).to.equal(0) | ||
367 | } | ||
368 | }) | ||
369 | }) | ||
370 | |||
371 | describe('Accept/update/abort/process a job', function () { | ||
372 | |||
373 | it('Should request available jobs', async function () { | ||
374 | lastRunnerContact = new Date() | ||
375 | |||
376 | const { availableJobs } = await server.runnerJobs.request({ runnerToken }) | ||
377 | |||
378 | // Only optimize jobs are available | ||
379 | expect(availableJobs).to.have.lengthOf(2) | ||
380 | |||
381 | for (const job of availableJobs) { | ||
382 | expect(job.uuid).to.exist | ||
383 | expect(job.payload.input).to.exist | ||
384 | expect((job.payload as RunnerJobVODWebVideoTranscodingPayload).output).to.exist | ||
385 | |||
386 | expect((job as RunnerJobAdmin).privatePayload).to.not.exist | ||
387 | } | ||
388 | |||
389 | const hlsJobs = availableJobs.filter(d => d.type === 'vod-hls-transcoding') | ||
390 | const webVideoJobs = availableJobs.filter(d => d.type === 'vod-web-video-transcoding') | ||
391 | |||
392 | expect(hlsJobs).to.have.lengthOf(0) | ||
393 | expect(webVideoJobs).to.have.lengthOf(2) | ||
394 | |||
395 | jobUUID = webVideoJobs[0].uuid | ||
396 | }) | ||
397 | |||
398 | it('Should have sorted available jobs by priority', async function () { | ||
399 | const { availableJobs } = await server.runnerJobs.request({ runnerToken }) | ||
400 | |||
401 | expect(availableJobs[0].uuid).to.equal(jobMaxPriority) | ||
402 | }) | ||
403 | |||
404 | it('Should have last runner contact updated', async function () { | ||
405 | await wait(1000) | ||
406 | |||
407 | const { data } = await server.runners.list({ sort: 'createdAt' }) | ||
408 | expect(new Date(data[0].lastContact)).to.be.above(lastRunnerContact) | ||
409 | }) | ||
410 | |||
411 | it('Should accept a job', async function () { | ||
412 | const startedAt = new Date() | ||
413 | |||
414 | const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID }) | ||
415 | jobToken = job.jobToken | ||
416 | |||
417 | const checkProcessingJob = (job: RunnerJob & { jobToken?: string }, fromAccept: boolean) => { | ||
418 | expect(job.uuid).to.equal(jobUUID) | ||
419 | |||
420 | expect(job.type).to.equal('vod-web-video-transcoding') | ||
421 | expect(job.state.label).to.equal('Processing') | ||
422 | expect(job.state.id).to.equal(RunnerJobState.PROCESSING) | ||
423 | |||
424 | expect(job.runner).to.exist | ||
425 | expect(job.runner.name).to.equal('runner 1') | ||
426 | expect(job.runner.description).to.equal('my super runner 1') | ||
427 | |||
428 | expect(job.progress).to.be.null | ||
429 | |||
430 | expect(job.startedAt).to.exist | ||
431 | expect(new Date(job.startedAt)).to.be.above(startedAt) | ||
432 | |||
433 | expect(job.finishedAt).to.not.exist | ||
434 | |||
435 | expect(job.failures).to.equal(0) | ||
436 | |||
437 | expect(job.payload).to.exist | ||
438 | |||
439 | if (fromAccept) { | ||
440 | expect(job.jobToken).to.exist | ||
441 | expect((job as RunnerJobAdmin).privatePayload).to.not.exist | ||
442 | } else { | ||
443 | expect(job.jobToken).to.not.exist | ||
444 | expect((job as RunnerJobAdmin).privatePayload).to.exist | ||
445 | } | ||
446 | } | ||
447 | |||
448 | checkProcessingJob(job, true) | ||
449 | |||
450 | const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' }) | ||
451 | |||
452 | const processingJob = data.find(j => j.uuid === jobUUID) | ||
453 | checkProcessingJob(processingJob, false) | ||
454 | |||
455 | await checkMainJobState(RunnerJobState.PROCESSING) | ||
456 | }) | ||
457 | |||
458 | it('Should update a job', async function () { | ||
459 | await server.runnerJobs.update({ runnerToken, jobUUID, jobToken, progress: 53 }) | ||
460 | |||
461 | const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' }) | ||
462 | |||
463 | for (const job of data) { | ||
464 | if (job.state.id === RunnerJobState.PROCESSING) { | ||
465 | expect(job.progress).to.equal(53) | ||
466 | } else { | ||
467 | expect(job.progress).to.be.null | ||
468 | } | ||
469 | } | ||
470 | }) | ||
471 | |||
472 | it('Should abort a job', async function () { | ||
473 | await server.runnerJobs.abort({ runnerToken, jobUUID, jobToken, reason: 'for tests' }) | ||
474 | |||
475 | await checkMainJobState(RunnerJobState.PENDING) | ||
476 | |||
477 | const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' }) | ||
478 | for (const job of data) { | ||
479 | expect(job.progress).to.be.null | ||
480 | } | ||
481 | }) | ||
482 | |||
483 | it('Should accept the same job again and post a success', async function () { | ||
484 | const { availableJobs } = await server.runnerJobs.request({ runnerToken }) | ||
485 | expect(availableJobs.find(j => j.uuid === jobUUID)).to.exist | ||
486 | |||
487 | const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID }) | ||
488 | jobToken = job.jobToken | ||
489 | |||
490 | await checkMainJobState(RunnerJobState.PROCESSING) | ||
491 | |||
492 | const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' }) | ||
493 | |||
494 | for (const job of data) { | ||
495 | expect(job.progress).to.be.null | ||
496 | } | ||
497 | |||
498 | const payload = { | ||
499 | videoFile: 'video_short.mp4' | ||
500 | } | ||
501 | |||
502 | await server.runnerJobs.success({ runnerToken, jobUUID, jobToken, payload }) | ||
503 | }) | ||
504 | |||
505 | it('Should not have available jobs anymore', async function () { | ||
506 | await checkMainJobState(RunnerJobState.COMPLETED) | ||
507 | |||
508 | const job = await getMainJob() | ||
509 | expect(job.finishedAt).to.exist | ||
510 | |||
511 | const { availableJobs } = await server.runnerJobs.request({ runnerToken }) | ||
512 | expect(availableJobs.find(j => j.uuid === jobUUID)).to.not.exist | ||
513 | }) | ||
514 | }) | ||
515 | |||
516 | describe('Error job', function () { | ||
517 | |||
518 | it('Should accept another job and post an error', async function () { | ||
519 | await server.runnerJobs.cancelAllJobs() | ||
520 | await server.videos.quickUpload({ name: 'video' }) | ||
521 | await waitJobs([ server ]) | ||
522 | |||
523 | const { availableJobs } = await server.runnerJobs.request({ runnerToken }) | ||
524 | jobUUID = availableJobs[0].uuid | ||
525 | |||
526 | const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID }) | ||
527 | jobToken = job.jobToken | ||
528 | |||
529 | await server.runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error' }) | ||
530 | }) | ||
531 | |||
532 | it('Should have job failures increased', async function () { | ||
533 | const job = await getMainJob() | ||
534 | expect(job.state.id).to.equal(RunnerJobState.PENDING) | ||
535 | expect(job.failures).to.equal(1) | ||
536 | expect(job.error).to.be.null | ||
537 | expect(job.progress).to.be.null | ||
538 | expect(job.finishedAt).to.not.exist | ||
539 | }) | ||
540 | |||
541 | it('Should error a job when job attempts is too big', async function () { | ||
542 | for (let i = 0; i < 4; i++) { | ||
543 | const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID }) | ||
544 | jobToken = job.jobToken | ||
545 | |||
546 | await server.runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error ' + i }) | ||
547 | } | ||
548 | |||
549 | const job = await getMainJob() | ||
550 | expect(job.failures).to.equal(5) | ||
551 | expect(job.state.id).to.equal(RunnerJobState.ERRORED) | ||
552 | expect(job.state.label).to.equal('Errored') | ||
553 | expect(job.error).to.equal('Error 3') | ||
554 | expect(job.progress).to.be.null | ||
555 | expect(job.finishedAt).to.exist | ||
556 | |||
557 | failedJob = job | ||
558 | }) | ||
559 | |||
560 | it('Should have failed children jobs too', async function () { | ||
561 | const { data } = await server.runnerJobs.list({ count: 50, sort: '-updatedAt' }) | ||
562 | |||
563 | const children = data.filter(j => j.parent?.uuid === failedJob.uuid) | ||
564 | expect(children).to.have.lengthOf(9) | ||
565 | |||
566 | for (const child of children) { | ||
567 | expect(child.parent.uuid).to.equal(failedJob.uuid) | ||
568 | expect(child.parent.type).to.equal(failedJob.type) | ||
569 | expect(child.parent.state.id).to.equal(failedJob.state.id) | ||
570 | expect(child.parent.state.label).to.equal(failedJob.state.label) | ||
571 | |||
572 | expect(child.state.id).to.equal(RunnerJobState.PARENT_ERRORED) | ||
573 | expect(child.state.label).to.equal('Parent job failed') | ||
574 | } | ||
575 | }) | ||
576 | }) | ||
577 | |||
578 | describe('Cancel', function () { | ||
579 | |||
580 | it('Should cancel a pending job', async function () { | ||
581 | await server.videos.quickUpload({ name: 'video' }) | ||
582 | await waitJobs([ server ]) | ||
583 | |||
584 | { | ||
585 | const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' }) | ||
586 | |||
587 | const pendingJob = data.find(j => j.state.id === RunnerJobState.PENDING) | ||
588 | jobUUID = pendingJob.uuid | ||
589 | |||
590 | await server.runnerJobs.cancelByAdmin({ jobUUID }) | ||
591 | } | ||
592 | |||
593 | { | ||
594 | const job = await getMainJob() | ||
595 | expect(job.state.id).to.equal(RunnerJobState.CANCELLED) | ||
596 | expect(job.state.label).to.equal('Cancelled') | ||
597 | } | ||
598 | |||
599 | { | ||
600 | const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' }) | ||
601 | const children = data.filter(j => j.parent?.uuid === jobUUID) | ||
602 | expect(children).to.have.lengthOf(9) | ||
603 | |||
604 | for (const child of children) { | ||
605 | expect(child.state.id).to.equal(RunnerJobState.PARENT_CANCELLED) | ||
606 | } | ||
607 | } | ||
608 | }) | ||
609 | |||
610 | it('Should cancel an already accepted job and skip success/error', async function () { | ||
611 | await server.videos.quickUpload({ name: 'video' }) | ||
612 | await waitJobs([ server ]) | ||
613 | |||
614 | const { availableJobs } = await server.runnerJobs.request({ runnerToken }) | ||
615 | jobUUID = availableJobs[0].uuid | ||
616 | |||
617 | const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID }) | ||
618 | jobToken = job.jobToken | ||
619 | |||
620 | await server.runnerJobs.cancelByAdmin({ jobUUID }) | ||
621 | |||
622 | await server.runnerJobs.abort({ runnerToken, jobUUID, jobToken, reason: 'aborted', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
623 | }) | ||
624 | }) | ||
625 | |||
626 | describe('Remove', function () { | ||
627 | |||
628 | it('Should remove a pending job', async function () { | ||
629 | await server.videos.quickUpload({ name: 'video' }) | ||
630 | await waitJobs([ server ]) | ||
631 | |||
632 | { | ||
633 | const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' }) | ||
634 | |||
635 | const pendingJob = data.find(j => j.state.id === RunnerJobState.PENDING) | ||
636 | jobUUID = pendingJob.uuid | ||
637 | |||
638 | await server.runnerJobs.deleteByAdmin({ jobUUID }) | ||
639 | } | ||
640 | |||
641 | { | ||
642 | const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' }) | ||
643 | |||
644 | const parent = data.find(j => j.uuid === jobUUID) | ||
645 | expect(parent).to.not.exist | ||
646 | |||
647 | const children = data.filter(j => j.parent?.uuid === jobUUID) | ||
648 | expect(children).to.have.lengthOf(0) | ||
649 | } | ||
650 | }) | ||
651 | }) | ||
652 | |||
653 | describe('Stalled jobs', function () { | ||
654 | |||
655 | it('Should abort stalled jobs', async function () { | ||
656 | this.timeout(60000) | ||
657 | |||
658 | await server.videos.quickUpload({ name: 'video' }) | ||
659 | await server.videos.quickUpload({ name: 'video' }) | ||
660 | await waitJobs([ server ]) | ||
661 | |||
662 | const { job: job1 } = await server.runnerJobs.autoAccept({ runnerToken }) | ||
663 | const { job: stalledJob } = await server.runnerJobs.autoAccept({ runnerToken }) | ||
664 | |||
665 | for (let i = 0; i < 6; i++) { | ||
666 | await wait(2000) | ||
667 | |||
668 | await server.runnerJobs.update({ runnerToken, jobToken: job1.jobToken, jobUUID: job1.uuid }) | ||
669 | } | ||
670 | |||
671 | const refreshedJob1 = await server.runnerJobs.getJob({ uuid: job1.uuid }) | ||
672 | const refreshedStalledJob = await server.runnerJobs.getJob({ uuid: stalledJob.uuid }) | ||
673 | |||
674 | expect(refreshedJob1.state.id).to.equal(RunnerJobState.PROCESSING) | ||
675 | expect(refreshedStalledJob.state.id).to.equal(RunnerJobState.PENDING) | ||
676 | }) | ||
677 | }) | ||
678 | |||
679 | describe('Rate limit', function () { | ||
680 | |||
681 | before(async function () { | ||
682 | this.timeout(60000) | ||
683 | |||
684 | await server.kill() | ||
685 | |||
686 | await server.run({ | ||
687 | rates_limit: { | ||
688 | api: { | ||
689 | max: 10 | ||
690 | } | ||
691 | } | ||
692 | }) | ||
693 | }) | ||
694 | |||
695 | it('Should rate limit an unknown runner, but not a registered one', async function () { | ||
696 | this.timeout(60000) | ||
697 | |||
698 | await server.videos.quickUpload({ name: 'video' }) | ||
699 | await waitJobs([ server ]) | ||
700 | |||
701 | const { job } = await server.runnerJobs.autoAccept({ runnerToken }) | ||
702 | |||
703 | for (let i = 0; i < 20; i++) { | ||
704 | try { | ||
705 | await server.runnerJobs.request({ runnerToken }) | ||
706 | await server.runnerJobs.update({ runnerToken, jobToken: job.jobToken, jobUUID: job.uuid }) | ||
707 | } catch {} | ||
708 | } | ||
709 | |||
710 | // Invalid | ||
711 | { | ||
712 | await server.runnerJobs.request({ runnerToken: 'toto', expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 }) | ||
713 | await server.runnerJobs.update({ | ||
714 | runnerToken: 'toto', | ||
715 | jobToken: job.jobToken, | ||
716 | jobUUID: job.uuid, | ||
717 | expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 | ||
718 | }) | ||
719 | } | ||
720 | |||
721 | // Not provided | ||
722 | { | ||
723 | await server.runnerJobs.request({ runnerToken: undefined, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 }) | ||
724 | await server.runnerJobs.update({ | ||
725 | runnerToken: undefined, | ||
726 | jobToken: job.jobToken, | ||
727 | jobUUID: job.uuid, | ||
728 | expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 | ||
729 | }) | ||
730 | } | ||
731 | |||
732 | // Registered | ||
733 | { | ||
734 | await server.runnerJobs.request({ runnerToken }) | ||
735 | await server.runnerJobs.update({ runnerToken, jobToken: job.jobToken, jobUUID: job.uuid }) | ||
736 | } | ||
737 | }) | ||
738 | }) | ||
739 | }) | ||
740 | |||
741 | after(async function () { | ||
742 | await cleanupTests([ server ]) | ||
743 | }) | ||
744 | }) | ||