]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/tests/api/runners/runner-common.ts
Bumped to version v5.2.1
[github/Chocobozzz/PeerTube.git] / server / tests / api / runners / runner-common.ts
CommitLineData
d102de1b
C
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@shared/core-utils'
5e47f6ab
C
5import {
6 HttpStatusCode,
7 Runner,
8 RunnerJob,
9 RunnerJobAdmin,
10 RunnerJobState,
11 RunnerJobVODWebVideoTranscodingPayload,
12 RunnerRegistrationToken
13} from '@shared/models'
d102de1b
C
14import {
15 cleanupTests,
16 createSingleServer,
d102de1b
C
17 PeerTubeServer,
18 setAccessTokensToServers,
19 setDefaultVideoChannel,
20 waitJobs
21} from '@shared/server-commands'
22
23describe('Test runner common actions', function () {
24 let server: PeerTubeServer
25 let registrationToken: string
26 let runnerToken: string
27 let jobMaxPriority: string
28
29 before(async function () {
30 this.timeout(120_000)
31
32 server = await createSingleServer(1, {
33 remote_runners: {
34 stalled_jobs: {
35 vod: '5 seconds'
36 }
37 }
38 })
39
40 await setAccessTokensToServers([ server ])
41 await setDefaultVideoChannel([ server ])
42
43 await server.config.enableTranscoding(true, true)
44 await server.config.enableRemoteTranscoding()
45 })
46
47 describe('Managing runner registration tokens', function () {
48 let base: RunnerRegistrationToken[]
49 let registrationTokenToDelete: RunnerRegistrationToken
50
51 it('Should have a default registration token', async function () {
52 const { total, data } = await server.runnerRegistrationTokens.list()
53
54 expect(total).to.equal(1)
55 expect(data).to.have.lengthOf(1)
56
57 const token = data[0]
58 expect(token.id).to.exist
59 expect(token.createdAt).to.exist
60 expect(token.updatedAt).to.exist
61 expect(token.registeredRunnersCount).to.equal(0)
62 expect(token.registrationToken).to.exist
63 })
64
65 it('Should create other registration tokens', async function () {
66 await server.runnerRegistrationTokens.generate()
67 await server.runnerRegistrationTokens.generate()
68
69 const { total, data } = await server.runnerRegistrationTokens.list()
70 expect(total).to.equal(3)
71 expect(data).to.have.lengthOf(3)
72 })
73
74 it('Should list registration tokens', async function () {
75 {
76 const { total, data } = await server.runnerRegistrationTokens.list({ sort: 'createdAt' })
77 expect(total).to.equal(3)
78 expect(data).to.have.lengthOf(3)
79 expect(new Date(data[0].createdAt)).to.be.below(new Date(data[1].createdAt))
80 expect(new Date(data[1].createdAt)).to.be.below(new Date(data[2].createdAt))
81
82 base = data
83
84 registrationTokenToDelete = data[0]
85 registrationToken = data[1].registrationToken
86 }
87
88 {
89 const { total, data } = await server.runnerRegistrationTokens.list({ sort: '-createdAt', start: 2, count: 1 })
90 expect(total).to.equal(3)
91 expect(data).to.have.lengthOf(1)
92 expect(data[0].registrationToken).to.equal(base[0].registrationToken)
93 }
94 })
95
96 it('Should have appropriate registeredRunnersCount for registration tokens', async function () {
97 await server.runners.register({ name: 'to delete 1', registrationToken: registrationTokenToDelete.registrationToken })
98 await server.runners.register({ name: 'to delete 2', registrationToken: registrationTokenToDelete.registrationToken })
99
100 const { data } = await server.runnerRegistrationTokens.list()
101
102 for (const d of data) {
103 if (d.registrationToken === registrationTokenToDelete.registrationToken) {
104 expect(d.registeredRunnersCount).to.equal(2)
105 } else {
106 expect(d.registeredRunnersCount).to.equal(0)
107 }
108 }
109
110 const { data: runners } = await server.runners.list()
111 expect(runners).to.have.lengthOf(2)
112 })
113
114 it('Should delete a registration token', async function () {
115 await server.runnerRegistrationTokens.delete({ id: registrationTokenToDelete.id })
116
117 const { total, data } = await server.runnerRegistrationTokens.list({ sort: 'createdAt' })
118 expect(total).to.equal(2)
119 expect(data).to.have.lengthOf(2)
120
121 for (const d of data) {
122 expect(d.registeredRunnersCount).to.equal(0)
123 expect(d.registrationToken).to.not.equal(registrationTokenToDelete.registrationToken)
124 }
125 })
126
127 it('Should have removed runners of this registration token', async function () {
128 const { data: runners } = await server.runners.list()
129 expect(runners).to.have.lengthOf(0)
130 })
131 })
132
133 describe('Managing runners', function () {
134 let toDelete: Runner
135
136 it('Should not have runners available', async function () {
137 const { total, data } = await server.runners.list()
138
139 expect(data).to.have.lengthOf(0)
140 expect(total).to.equal(0)
141 })
142
143 it('Should register runners', async function () {
144 const now = new Date()
145
146 const result = await server.runners.register({
147 name: 'runner 1',
148 description: 'my super runner 1',
149 registrationToken
150 })
151 expect(result.runnerToken).to.exist
152 runnerToken = result.runnerToken
153
154 await server.runners.register({
155 name: 'runner 2',
156 registrationToken
157 })
158
159 const { total, data } = await server.runners.list({ sort: 'createdAt' })
160 expect(total).to.equal(2)
161 expect(data).to.have.lengthOf(2)
162
163 for (const d of data) {
164 expect(d.id).to.exist
165 expect(d.createdAt).to.exist
166 expect(d.updatedAt).to.exist
167 expect(new Date(d.createdAt)).to.be.above(now)
168 expect(new Date(d.updatedAt)).to.be.above(now)
169 expect(new Date(d.lastContact)).to.be.above(now)
170 expect(d.ip).to.exist
171 }
172
173 expect(data[0].name).to.equal('runner 1')
174 expect(data[0].description).to.equal('my super runner 1')
175
176 expect(data[1].name).to.equal('runner 2')
177 expect(data[1].description).to.be.null
178
179 toDelete = data[1]
180 })
181
182 it('Should list runners', async function () {
183 const { total, data } = await server.runners.list({ sort: '-createdAt', start: 1, count: 1 })
184
185 expect(total).to.equal(2)
186 expect(data).to.have.lengthOf(1)
187 expect(data[0].name).to.equal('runner 1')
188 })
189
190 it('Should delete a runner', async function () {
191 await server.runners.delete({ id: toDelete.id })
192
193 const { total, data } = await server.runners.list()
194
195 expect(total).to.equal(1)
196 expect(data).to.have.lengthOf(1)
197 expect(data[0].name).to.equal('runner 1')
198 })
199
200 it('Should unregister a runner', async function () {
201 const registered = await server.runners.autoRegisterRunner()
202
203 {
204 const { total, data } = await server.runners.list()
205 expect(total).to.equal(2)
206 expect(data).to.have.lengthOf(2)
207 }
208
209 await server.runners.unregister({ runnerToken: registered })
210
211 {
212 const { total, data } = await server.runners.list()
213 expect(total).to.equal(1)
214 expect(data).to.have.lengthOf(1)
215 expect(data[0].name).to.equal('runner 1')
216 }
217 })
218 })
219
220 describe('Managing runner jobs', function () {
221 let jobUUID: string
222 let jobToken: string
223 let lastRunnerContact: Date
224 let failedJob: RunnerJob
225
226 async function checkMainJobState (
227 mainJobState: RunnerJobState,
228 otherJobStates: RunnerJobState[] = [ RunnerJobState.PENDING, RunnerJobState.WAITING_FOR_PARENT_JOB ]
229 ) {
230 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
231
232 for (const job of data) {
233 if (job.uuid === jobUUID) {
234 expect(job.state.id).to.equal(mainJobState)
235 } else {
236 expect(otherJobStates).to.include(job.state.id)
237 }
238 }
239 }
240
241 function getMainJob () {
242 return server.runnerJobs.getJob({ uuid: jobUUID })
243 }
244
245 describe('List jobs', function () {
246
247 it('Should not have jobs', async function () {
248 const { total, data } = await server.runnerJobs.list()
249
250 expect(data).to.have.lengthOf(0)
251 expect(total).to.equal(0)
252 })
253
254 it('Should upload a video and have available jobs', async function () {
255 await server.videos.quickUpload({ name: 'to transcode' })
256 await waitJobs([ server ])
257
258 const { total, data } = await server.runnerJobs.list()
259
260 expect(data).to.have.lengthOf(10)
261 expect(total).to.equal(10)
262
263 for (const job of data) {
264 expect(job.startedAt).to.not.exist
265 expect(job.finishedAt).to.not.exist
266 expect(job.payload).to.exist
267 expect(job.privatePayload).to.exist
268 }
269
270 const hlsJobs = data.filter(d => d.type === 'vod-hls-transcoding')
271 const webVideoJobs = data.filter(d => d.type === 'vod-web-video-transcoding')
272
273 expect(hlsJobs).to.have.lengthOf(5)
274 expect(webVideoJobs).to.have.lengthOf(5)
275
276 const pendingJobs = data.filter(d => d.state.id === RunnerJobState.PENDING)
277 const waitingJobs = data.filter(d => d.state.id === RunnerJobState.WAITING_FOR_PARENT_JOB)
278
279 expect(pendingJobs).to.have.lengthOf(1)
280 expect(waitingJobs).to.have.lengthOf(9)
281 })
282
283 it('Should upload another video and list/sort jobs', async function () {
284 await server.videos.quickUpload({ name: 'to transcode 2' })
285 await waitJobs([ server ])
286
287 {
288 const { total, data } = await server.runnerJobs.list({ start: 0, count: 30 })
289
290 expect(data).to.have.lengthOf(20)
291 expect(total).to.equal(20)
292
293 jobUUID = data[16].uuid
294 }
295
296 {
297 const { total, data } = await server.runnerJobs.list({ start: 3, count: 1, sort: 'createdAt' })
298 expect(total).to.equal(20)
299
300 expect(data).to.have.lengthOf(1)
301 expect(data[0].uuid).to.equal(jobUUID)
302 }
303
304 {
305 let previousPriority = Infinity
306 const { total, data } = await server.runnerJobs.list({ start: 0, count: 100, sort: '-priority' })
307 expect(total).to.equal(20)
308
309 for (const job of data) {
310 expect(job.priority).to.be.at.most(previousPriority)
311 previousPriority = job.priority
312
313 if (job.state.id === RunnerJobState.PENDING) {
314 jobMaxPriority = job.uuid
315 }
316 }
317 }
318 })
319
320 it('Should search jobs', async function () {
321 {
322 const { total, data } = await server.runnerJobs.list({ search: jobUUID })
323
324 expect(data).to.have.lengthOf(1)
325 expect(total).to.equal(1)
326
327 expect(data[0].uuid).to.equal(jobUUID)
328 }
329
330 {
331 const { total, data } = await server.runnerJobs.list({ search: 'toto' })
332
333 expect(data).to.have.lengthOf(0)
334 expect(total).to.equal(0)
335 }
336
337 {
338 const { total, data } = await server.runnerJobs.list({ search: 'hls' })
339
340 expect(data).to.not.have.lengthOf(0)
341 expect(total).to.not.equal(0)
342 }
343 })
344 })
345
346 describe('Accept/update/abort/process a job', function () {
347
348 it('Should request available jobs', async function () {
349 lastRunnerContact = new Date()
350
351 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
352
353 // Only optimize jobs are available
354 expect(availableJobs).to.have.lengthOf(2)
355
356 for (const job of availableJobs) {
357 expect(job.uuid).to.exist
358 expect(job.payload.input).to.exist
5e47f6ab 359 expect((job.payload as RunnerJobVODWebVideoTranscodingPayload).output).to.exist
d102de1b
C
360
361 expect((job as RunnerJobAdmin).privatePayload).to.not.exist
362 }
363
364 const hlsJobs = availableJobs.filter(d => d.type === 'vod-hls-transcoding')
365 const webVideoJobs = availableJobs.filter(d => d.type === 'vod-web-video-transcoding')
366
367 expect(hlsJobs).to.have.lengthOf(0)
368 expect(webVideoJobs).to.have.lengthOf(2)
369
370 jobUUID = webVideoJobs[0].uuid
371 })
372
373 it('Should have sorted available jobs by priority', async function () {
374 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
375
376 expect(availableJobs[0].uuid).to.equal(jobMaxPriority)
377 })
378
379 it('Should have last runner contact updated', async function () {
380 await wait(1000)
381
382 const { data } = await server.runners.list({ sort: 'createdAt' })
383 expect(new Date(data[0].lastContact)).to.be.above(lastRunnerContact)
384 })
385
386 it('Should accept a job', async function () {
387 const startedAt = new Date()
388
389 const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
390 jobToken = job.jobToken
391
392 const checkProcessingJob = (job: RunnerJob & { jobToken?: string }, fromAccept: boolean) => {
393 expect(job.uuid).to.equal(jobUUID)
394
395 expect(job.type).to.equal('vod-web-video-transcoding')
396 expect(job.state.label).to.equal('Processing')
397 expect(job.state.id).to.equal(RunnerJobState.PROCESSING)
398
399 expect(job.runner).to.exist
400 expect(job.runner.name).to.equal('runner 1')
401 expect(job.runner.description).to.equal('my super runner 1')
402
403 expect(job.progress).to.be.null
404
405 expect(job.startedAt).to.exist
406 expect(new Date(job.startedAt)).to.be.above(startedAt)
407
408 expect(job.finishedAt).to.not.exist
409
410 expect(job.failures).to.equal(0)
411
412 expect(job.payload).to.exist
413
414 if (fromAccept) {
415 expect(job.jobToken).to.exist
416 expect((job as RunnerJobAdmin).privatePayload).to.not.exist
417 } else {
418 expect(job.jobToken).to.not.exist
419 expect((job as RunnerJobAdmin).privatePayload).to.exist
420 }
421 }
422
423 checkProcessingJob(job, true)
424
425 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
426
427 const processingJob = data.find(j => j.uuid === jobUUID)
428 checkProcessingJob(processingJob, false)
429
430 await checkMainJobState(RunnerJobState.PROCESSING)
431 })
432
433 it('Should update a job', async function () {
434 await server.runnerJobs.update({ runnerToken, jobUUID, jobToken, progress: 53 })
435
436 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
437
438 for (const job of data) {
439 if (job.state.id === RunnerJobState.PROCESSING) {
440 expect(job.progress).to.equal(53)
441 } else {
442 expect(job.progress).to.be.null
443 }
444 }
445 })
446
447 it('Should abort a job', async function () {
448 await server.runnerJobs.abort({ runnerToken, jobUUID, jobToken, reason: 'for tests' })
449
450 await checkMainJobState(RunnerJobState.PENDING)
451
452 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
453 for (const job of data) {
454 expect(job.progress).to.be.null
455 }
456 })
457
458 it('Should accept the same job again and post a success', async function () {
459 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
460 expect(availableJobs.find(j => j.uuid === jobUUID)).to.exist
461
462 const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
463 jobToken = job.jobToken
464
465 await checkMainJobState(RunnerJobState.PROCESSING)
466
467 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
468
469 for (const job of data) {
470 expect(job.progress).to.be.null
471 }
472
473 const payload = {
474 videoFile: 'video_short.mp4'
475 }
476
477 await server.runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
478 })
479
480 it('Should not have available jobs anymore', async function () {
481 await checkMainJobState(RunnerJobState.COMPLETED)
482
483 const job = await getMainJob()
484 expect(job.finishedAt).to.exist
485
486 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
487 expect(availableJobs.find(j => j.uuid === jobUUID)).to.not.exist
488 })
489 })
490
491 describe('Error job', function () {
492
493 it('Should accept another job and post an error', async function () {
494 await server.runnerJobs.cancelAllJobs()
495 await server.videos.quickUpload({ name: 'video' })
496 await waitJobs([ server ])
497
498 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
499 jobUUID = availableJobs[0].uuid
500
501 const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
502 jobToken = job.jobToken
503
504 await server.runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error' })
505 })
506
507 it('Should have job failures increased', async function () {
508 const job = await getMainJob()
509 expect(job.state.id).to.equal(RunnerJobState.PENDING)
510 expect(job.failures).to.equal(1)
511 expect(job.error).to.be.null
512 expect(job.progress).to.be.null
513 expect(job.finishedAt).to.not.exist
514 })
515
516 it('Should error a job when job attempts is too big', async function () {
517 for (let i = 0; i < 4; i++) {
518 const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
519 jobToken = job.jobToken
520
521 await server.runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error ' + i })
522 }
523
524 const job = await getMainJob()
525 expect(job.failures).to.equal(5)
526 expect(job.state.id).to.equal(RunnerJobState.ERRORED)
527 expect(job.state.label).to.equal('Errored')
528 expect(job.error).to.equal('Error 3')
529 expect(job.progress).to.be.null
530 expect(job.finishedAt).to.exist
531
532 failedJob = job
533 })
534
535 it('Should have failed children jobs too', async function () {
536 const { data } = await server.runnerJobs.list({ count: 50, sort: '-updatedAt' })
537
538 const children = data.filter(j => j.parent?.uuid === failedJob.uuid)
539 expect(children).to.have.lengthOf(9)
540
541 for (const child of children) {
542 expect(child.parent.uuid).to.equal(failedJob.uuid)
543 expect(child.parent.type).to.equal(failedJob.type)
544 expect(child.parent.state.id).to.equal(failedJob.state.id)
545 expect(child.parent.state.label).to.equal(failedJob.state.label)
546
547 expect(child.state.id).to.equal(RunnerJobState.PARENT_ERRORED)
548 expect(child.state.label).to.equal('Parent job failed')
549 }
550 })
551 })
552
553 describe('Cancel', function () {
554
555 it('Should cancel a pending job', async function () {
556 await server.videos.quickUpload({ name: 'video' })
557 await waitJobs([ server ])
558
559 {
560 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
561
562 const pendingJob = data.find(j => j.state.id === RunnerJobState.PENDING)
563 jobUUID = pendingJob.uuid
564
565 await server.runnerJobs.cancelByAdmin({ jobUUID })
566 }
567
568 {
569 const job = await getMainJob()
570 expect(job.state.id).to.equal(RunnerJobState.CANCELLED)
571 expect(job.state.label).to.equal('Cancelled')
572 }
573
574 {
575 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
576 const children = data.filter(j => j.parent?.uuid === jobUUID)
577 expect(children).to.have.lengthOf(9)
578
579 for (const child of children) {
580 expect(child.state.id).to.equal(RunnerJobState.PARENT_CANCELLED)
581 }
582 }
583 })
584
585 it('Should cancel an already accepted job and skip success/error', async function () {
586 await server.videos.quickUpload({ name: 'video' })
587 await waitJobs([ server ])
588
589 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
590 jobUUID = availableJobs[0].uuid
591
592 const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
593 jobToken = job.jobToken
594
595 await server.runnerJobs.cancelByAdmin({ jobUUID })
596
597 await server.runnerJobs.abort({ runnerToken, jobUUID, jobToken, reason: 'aborted', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
598 })
599 })
600
601 describe('Stalled jobs', function () {
602
603 it('Should abort stalled jobs', async function () {
604 this.timeout(60000)
605
606 await server.videos.quickUpload({ name: 'video' })
607 await server.videos.quickUpload({ name: 'video' })
608 await waitJobs([ server ])
609
610 const { job: job1 } = await server.runnerJobs.autoAccept({ runnerToken })
611 const { job: stalledJob } = await server.runnerJobs.autoAccept({ runnerToken })
612
613 for (let i = 0; i < 6; i++) {
614 await wait(2000)
615
616 await server.runnerJobs.update({ runnerToken, jobToken: job1.jobToken, jobUUID: job1.uuid })
617 }
618
619 const refreshedJob1 = await server.runnerJobs.getJob({ uuid: job1.uuid })
620 const refreshedStalledJob = await server.runnerJobs.getJob({ uuid: stalledJob.uuid })
621
622 expect(refreshedJob1.state.id).to.equal(RunnerJobState.PROCESSING)
623 expect(refreshedStalledJob.state.id).to.equal(RunnerJobState.PENDING)
624 })
625 })
626
627 describe('Rate limit', function () {
628
629 before(async function () {
630 this.timeout(60000)
631
632 await server.kill()
633
634 await server.run({
635 rates_limit: {
636 api: {
637 max: 10
638 }
639 }
640 })
641 })
642
e915cde3
C
643 it('Should rate limit an unknown runner, but not a registered one', async function () {
644 this.timeout(60000)
645
646 await server.videos.quickUpload({ name: 'video' })
647 await waitJobs([ server ])
648
649 const { job } = await server.runnerJobs.autoAccept({ runnerToken })
d102de1b
C
650
651 for (let i = 0; i < 20; i++) {
652 try {
e915cde3
C
653 await server.runnerJobs.request({ runnerToken })
654 await server.runnerJobs.update({ runnerToken, jobToken: job.jobToken, jobUUID: job.uuid })
d102de1b
C
655 } catch {}
656 }
657
e915cde3
C
658 // Invalid
659 {
660 await server.runnerJobs.request({ runnerToken: 'toto', expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
661 await server.runnerJobs.update({
662 runnerToken: 'toto',
663 jobToken: job.jobToken,
664 jobUUID: job.uuid,
665 expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429
666 })
667 }
d102de1b 668
e915cde3
C
669 // Not provided
670 {
671 await server.runnerJobs.request({ runnerToken: undefined, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
672 await server.runnerJobs.update({
673 runnerToken: undefined,
674 jobToken: job.jobToken,
675 jobUUID: job.uuid,
676 expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429
677 })
678 }
d102de1b 679
e915cde3
C
680 // Registered
681 {
682 await server.runnerJobs.request({ runnerToken })
683 await server.runnerJobs.update({ runnerToken, jobToken: job.jobToken, jobUUID: job.uuid })
d102de1b
C
684 }
685 })
686 })
687 })
688
689 after(async function () {
690 await cleanupTests([ server ])
691 })
692})