]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame_incremental - server/tests/api/redundancy/redundancy.ts
Fix tests
[github/Chocobozzz/PeerTube.git] / server / tests / api / redundancy / redundancy.ts
... / ...
CommitLineData
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import * as chai from 'chai'
5import { readdir } from 'fs-extra'
6import * as magnetUtil from 'magnet-uri'
7import { join } from 'path'
8import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
9import {
10 checkSegmentHash,
11 checkVideoFilesWereRemoved,
12 cleanupTests,
13 doubleFollow,
14 flushAndRunMultipleServers,
15 getFollowingListPaginationAndSort,
16 getVideo,
17 getVideoWithToken,
18 immutableAssign,
19 killallServers,
20 makeGetRequest,
21 removeVideo,
22 reRunServer,
23 root,
24 ServerInfo,
25 setAccessTokensToServers,
26 unfollow,
27 uploadVideo,
28 viewVideo,
29 wait,
30 waitUntilLog
31} from '../../../../shared/extra-utils'
32import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
33import {
34 addVideoRedundancy,
35 listVideoRedundancies,
36 removeVideoRedundancy,
37 updateRedundancy
38} from '../../../../shared/extra-utils/server/redundancy'
39import { getStats } from '../../../../shared/extra-utils/server/stats'
40import { ActorFollow } from '../../../../shared/models/actors'
41import { VideoRedundancy, VideoRedundancyStrategy, VideoRedundancyStrategyWithManual } from '../../../../shared/models/redundancy'
42import { ServerStats } from '../../../../shared/models/server/server-stats.model'
43import { VideoDetails } from '../../../../shared/models/videos'
44
45const expect = chai.expect
46
47let servers: ServerInfo[] = []
48let video1Server2UUID: string
49let video1Server2Id: number
50
51function checkMagnetWebseeds (file: { magnetUri: string, resolution: { id: number } }, baseWebseeds: string[], server: ServerInfo) {
52 const parsed = magnetUtil.decode(file.magnetUri)
53
54 for (const ws of baseWebseeds) {
55 const found = parsed.urlList.find(url => url === `${ws}-${file.resolution.id}.mp4`)
56 expect(found, `Webseed ${ws} not found in ${file.magnetUri} on server ${server.url}`).to.not.be.undefined
57 }
58
59 expect(parsed.urlList).to.have.lengthOf(baseWebseeds.length)
60}
61
62async function flushAndRunServers (strategy: VideoRedundancyStrategy | null, additionalParams: any = {}, withWebtorrent = true) {
63 const strategies: any[] = []
64
65 if (strategy !== null) {
66 strategies.push(
67 immutableAssign({
68 min_lifetime: '1 hour',
69 strategy: strategy,
70 size: '400KB'
71 }, additionalParams)
72 )
73 }
74
75 const config = {
76 transcoding: {
77 webtorrent: {
78 enabled: withWebtorrent
79 },
80 hls: {
81 enabled: true
82 }
83 },
84 redundancy: {
85 videos: {
86 check_interval: '5 seconds',
87 strategies
88 }
89 }
90 }
91
92 servers = await flushAndRunMultipleServers(3, config)
93
94 // Get the access tokens
95 await setAccessTokensToServers(servers)
96
97 {
98 const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video 1 server 2' })
99 video1Server2UUID = res.body.video.uuid
100 video1Server2Id = res.body.video.id
101
102 await viewVideo(servers[1].url, video1Server2UUID)
103 }
104
105 await waitJobs(servers)
106
107 // Server 1 and server 2 follow each other
108 await doubleFollow(servers[0], servers[1])
109 // Server 1 and server 3 follow each other
110 await doubleFollow(servers[0], servers[2])
111 // Server 2 and server 3 follow each other
112 await doubleFollow(servers[1], servers[2])
113
114 await waitJobs(servers)
115}
116
117async function check1WebSeed (videoUUID?: string) {
118 if (!videoUUID) videoUUID = video1Server2UUID
119
120 const webseeds = [
121 `http://localhost:${servers[1].port}/static/webseed/${videoUUID}`
122 ]
123
124 for (const server of servers) {
125 // With token to avoid issues with video follow constraints
126 const res = await getVideoWithToken(server.url, server.accessToken, videoUUID)
127
128 const video: VideoDetails = res.body
129 for (const f of video.files) {
130 checkMagnetWebseeds(f, webseeds, server)
131 }
132 }
133}
134
135async function check2Webseeds (videoUUID?: string) {
136 if (!videoUUID) videoUUID = video1Server2UUID
137
138 const webseeds = [
139 `http://localhost:${servers[0].port}/static/redundancy/${videoUUID}`,
140 `http://localhost:${servers[1].port}/static/webseed/${videoUUID}`
141 ]
142
143 for (const server of servers) {
144 const res = await getVideo(server.url, videoUUID)
145
146 const video: VideoDetails = res.body
147
148 for (const file of video.files) {
149 checkMagnetWebseeds(file, webseeds, server)
150
151 await makeGetRequest({
152 url: servers[0].url,
153 statusCodeExpected: HttpStatusCode.OK_200,
154 path: '/static/redundancy/' + `${videoUUID}-${file.resolution.id}.mp4`,
155 contentType: null
156 })
157 await makeGetRequest({
158 url: servers[1].url,
159 statusCodeExpected: HttpStatusCode.OK_200,
160 path: `/static/webseed/${videoUUID}-${file.resolution.id}.mp4`,
161 contentType: null
162 })
163 }
164 }
165
166 const directories = [
167 'test' + servers[0].internalServerNumber + '/redundancy',
168 'test' + servers[1].internalServerNumber + '/videos'
169 ]
170
171 for (const directory of directories) {
172 const files = await readdir(join(root(), directory))
173 expect(files).to.have.length.at.least(4)
174
175 for (const resolution of [ 240, 360, 480, 720 ]) {
176 expect(files.find(f => f === `${videoUUID}-${resolution}.mp4`)).to.not.be.undefined
177 }
178 }
179}
180
181async function check0PlaylistRedundancies (videoUUID?: string) {
182 if (!videoUUID) videoUUID = video1Server2UUID
183
184 for (const server of servers) {
185 // With token to avoid issues with video follow constraints
186 const res = await getVideoWithToken(server.url, server.accessToken, videoUUID)
187 const video: VideoDetails = res.body
188
189 expect(video.streamingPlaylists).to.be.an('array')
190 expect(video.streamingPlaylists).to.have.lengthOf(1)
191 expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(0)
192 }
193}
194
195async function check1PlaylistRedundancies (videoUUID?: string) {
196 if (!videoUUID) videoUUID = video1Server2UUID
197
198 for (const server of servers) {
199 const res = await getVideo(server.url, videoUUID)
200 const video: VideoDetails = res.body
201
202 expect(video.streamingPlaylists).to.have.lengthOf(1)
203 expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(1)
204
205 const redundancy = video.streamingPlaylists[0].redundancies[0]
206
207 expect(redundancy.baseUrl).to.equal(servers[0].url + '/static/redundancy/hls/' + videoUUID)
208 }
209
210 const baseUrlPlaylist = servers[1].url + '/static/streaming-playlists/hls'
211 const baseUrlSegment = servers[0].url + '/static/redundancy/hls'
212
213 const res = await getVideo(servers[0].url, videoUUID)
214 const hlsPlaylist = (res.body as VideoDetails).streamingPlaylists[0]
215
216 for (const resolution of [ 240, 360, 480, 720 ]) {
217 await checkSegmentHash(baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist)
218 }
219
220 const directories = [
221 'test' + servers[0].internalServerNumber + '/redundancy/hls',
222 'test' + servers[1].internalServerNumber + '/streaming-playlists/hls'
223 ]
224
225 for (const directory of directories) {
226 const files = await readdir(join(root(), directory, videoUUID))
227 expect(files).to.have.length.at.least(4)
228
229 for (const resolution of [ 240, 360, 480, 720 ]) {
230 const filename = `${videoUUID}-${resolution}-fragmented.mp4`
231
232 expect(files.find(f => f === filename)).to.not.be.undefined
233 }
234 }
235}
236
237async function checkStatsGlobal (strategy: VideoRedundancyStrategyWithManual) {
238 let totalSize: number = null
239 let statsLength = 1
240
241 if (strategy !== 'manual') {
242 totalSize = 409600
243 statsLength = 2
244 }
245
246 const res = await getStats(servers[0].url)
247 const data: ServerStats = res.body
248
249 expect(data.videosRedundancy).to.have.lengthOf(statsLength)
250
251 const stat = data.videosRedundancy[0]
252 expect(stat.strategy).to.equal(strategy)
253 expect(stat.totalSize).to.equal(totalSize)
254
255 return stat
256}
257
258async function checkStatsWith1Redundancy (strategy: VideoRedundancyStrategyWithManual, onlyHls = false) {
259 const stat = await checkStatsGlobal(strategy)
260
261 expect(stat.totalUsed).to.be.at.least(1).and.below(409601)
262 expect(stat.totalVideoFiles).to.equal(onlyHls ? 4 : 8)
263 expect(stat.totalVideos).to.equal(1)
264}
265
266async function checkStatsWithoutRedundancy (strategy: VideoRedundancyStrategyWithManual) {
267 const stat = await checkStatsGlobal(strategy)
268
269 expect(stat.totalUsed).to.equal(0)
270 expect(stat.totalVideoFiles).to.equal(0)
271 expect(stat.totalVideos).to.equal(0)
272}
273
274async function enableRedundancyOnServer1 () {
275 await updateRedundancy(servers[0].url, servers[0].accessToken, servers[1].host, true)
276
277 const res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 0, count: 5, sort: '-createdAt' })
278 const follows: ActorFollow[] = res.body.data
279 const server2 = follows.find(f => f.following.host === `localhost:${servers[1].port}`)
280 const server3 = follows.find(f => f.following.host === `localhost:${servers[2].port}`)
281
282 expect(server3).to.not.be.undefined
283 expect(server3.following.hostRedundancyAllowed).to.be.false
284
285 expect(server2).to.not.be.undefined
286 expect(server2.following.hostRedundancyAllowed).to.be.true
287}
288
289async function disableRedundancyOnServer1 () {
290 await updateRedundancy(servers[0].url, servers[0].accessToken, servers[1].host, false)
291
292 const res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 0, count: 5, sort: '-createdAt' })
293 const follows: ActorFollow[] = res.body.data
294 const server2 = follows.find(f => f.following.host === `localhost:${servers[1].port}`)
295 const server3 = follows.find(f => f.following.host === `localhost:${servers[2].port}`)
296
297 expect(server3).to.not.be.undefined
298 expect(server3.following.hostRedundancyAllowed).to.be.false
299
300 expect(server2).to.not.be.undefined
301 expect(server2.following.hostRedundancyAllowed).to.be.false
302}
303
304describe('Test videos redundancy', function () {
305
306 describe('With most-views strategy', function () {
307 const strategy = 'most-views'
308
309 before(function () {
310 this.timeout(120000)
311
312 return flushAndRunServers(strategy)
313 })
314
315 it('Should have 1 webseed on the first video', async function () {
316 await check1WebSeed()
317 await check0PlaylistRedundancies()
318 await checkStatsWithoutRedundancy(strategy)
319 })
320
321 it('Should enable redundancy on server 1', function () {
322 return enableRedundancyOnServer1()
323 })
324
325 it('Should have 2 webseeds on the first video', async function () {
326 this.timeout(80000)
327
328 await waitJobs(servers)
329 await waitUntilLog(servers[0], 'Duplicated ', 5)
330 await waitJobs(servers)
331
332 await check2Webseeds()
333 await check1PlaylistRedundancies()
334 await checkStatsWith1Redundancy(strategy)
335 })
336
337 it('Should undo redundancy on server 1 and remove duplicated videos', async function () {
338 this.timeout(80000)
339
340 await disableRedundancyOnServer1()
341
342 await waitJobs(servers)
343 await wait(5000)
344
345 await check1WebSeed()
346 await check0PlaylistRedundancies()
347
348 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].internalServerNumber, [ 'videos', join('playlists', 'hls') ])
349 })
350
351 after(async function () {
352 return cleanupTests(servers)
353 })
354 })
355
356 describe('With trending strategy', function () {
357 const strategy = 'trending'
358
359 before(function () {
360 this.timeout(120000)
361
362 return flushAndRunServers(strategy)
363 })
364
365 it('Should have 1 webseed on the first video', async function () {
366 await check1WebSeed()
367 await check0PlaylistRedundancies()
368 await checkStatsWithoutRedundancy(strategy)
369 })
370
371 it('Should enable redundancy on server 1', function () {
372 return enableRedundancyOnServer1()
373 })
374
375 it('Should have 2 webseeds on the first video', async function () {
376 this.timeout(80000)
377
378 await waitJobs(servers)
379 await waitUntilLog(servers[0], 'Duplicated ', 5)
380 await waitJobs(servers)
381
382 await check2Webseeds()
383 await check1PlaylistRedundancies()
384 await checkStatsWith1Redundancy(strategy)
385 })
386
387 it('Should unfollow on server 1 and remove duplicated videos', async function () {
388 this.timeout(80000)
389
390 await unfollow(servers[0].url, servers[0].accessToken, servers[1])
391
392 await waitJobs(servers)
393 await wait(5000)
394
395 await check1WebSeed()
396 await check0PlaylistRedundancies()
397
398 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].internalServerNumber, [ 'videos' ])
399 })
400
401 after(async function () {
402 await cleanupTests(servers)
403 })
404 })
405
406 describe('With recently added strategy', function () {
407 const strategy = 'recently-added'
408
409 before(function () {
410 this.timeout(120000)
411
412 return flushAndRunServers(strategy, { min_views: 3 })
413 })
414
415 it('Should have 1 webseed on the first video', async function () {
416 await check1WebSeed()
417 await check0PlaylistRedundancies()
418 await checkStatsWithoutRedundancy(strategy)
419 })
420
421 it('Should enable redundancy on server 1', function () {
422 return enableRedundancyOnServer1()
423 })
424
425 it('Should still have 1 webseed on the first video', async function () {
426 this.timeout(80000)
427
428 await waitJobs(servers)
429 await wait(15000)
430 await waitJobs(servers)
431
432 await check1WebSeed()
433 await check0PlaylistRedundancies()
434 await checkStatsWithoutRedundancy(strategy)
435 })
436
437 it('Should view 2 times the first video to have > min_views config', async function () {
438 this.timeout(80000)
439
440 await viewVideo(servers[0].url, video1Server2UUID)
441 await viewVideo(servers[2].url, video1Server2UUID)
442
443 await wait(10000)
444 await waitJobs(servers)
445 })
446
447 it('Should have 2 webseeds on the first video', async function () {
448 this.timeout(80000)
449
450 await waitJobs(servers)
451 await waitUntilLog(servers[0], 'Duplicated ', 5)
452 await waitJobs(servers)
453
454 await check2Webseeds()
455 await check1PlaylistRedundancies()
456 await checkStatsWith1Redundancy(strategy)
457 })
458
459 it('Should remove the video and the redundancy files', async function () {
460 this.timeout(20000)
461
462 await removeVideo(servers[1].url, servers[1].accessToken, video1Server2UUID)
463
464 await waitJobs(servers)
465
466 for (const server of servers) {
467 await checkVideoFilesWereRemoved(video1Server2UUID, server.internalServerNumber)
468 }
469 })
470
471 after(async function () {
472 await cleanupTests(servers)
473 })
474 })
475
476 describe('With only HLS files', function () {
477 const strategy = 'recently-added'
478
479 before(async function () {
480 this.timeout(120000)
481
482 await flushAndRunServers(strategy, { min_views: 3 }, false)
483 })
484
485 it('Should have 0 playlist redundancy on the first video', async function () {
486 await check1WebSeed()
487 await check0PlaylistRedundancies()
488 })
489
490 it('Should enable redundancy on server 1', function () {
491 return enableRedundancyOnServer1()
492 })
493
494 it('Should still have 0 redundancy on the first video', async function () {
495 this.timeout(80000)
496
497 await waitJobs(servers)
498 await wait(15000)
499 await waitJobs(servers)
500
501 await check0PlaylistRedundancies()
502 await checkStatsWithoutRedundancy(strategy)
503 })
504
505 it('Should have 1 redundancy on the first video', async function () {
506 this.timeout(160000)
507
508 await viewVideo(servers[0].url, video1Server2UUID)
509 await viewVideo(servers[2].url, video1Server2UUID)
510
511 await wait(10000)
512 await waitJobs(servers)
513
514 await waitJobs(servers)
515 await waitUntilLog(servers[0], 'Duplicated ', 1)
516 await waitJobs(servers)
517
518 await check1PlaylistRedundancies()
519 await checkStatsWith1Redundancy(strategy, true)
520 })
521
522 it('Should remove the video and the redundancy files', async function () {
523 this.timeout(20000)
524
525 await removeVideo(servers[1].url, servers[1].accessToken, video1Server2UUID)
526
527 await waitJobs(servers)
528
529 for (const server of servers) {
530 await checkVideoFilesWereRemoved(video1Server2UUID, server.internalServerNumber)
531 }
532 })
533
534 after(async function () {
535 await cleanupTests(servers)
536 })
537 })
538
539 describe('With manual strategy', function () {
540 before(function () {
541 this.timeout(120000)
542
543 return flushAndRunServers(null)
544 })
545
546 it('Should have 1 webseed on the first video', async function () {
547 await check1WebSeed()
548 await check0PlaylistRedundancies()
549 await checkStatsWithoutRedundancy('manual')
550 })
551
552 it('Should create a redundancy on first video', async function () {
553 await addVideoRedundancy({
554 url: servers[0].url,
555 accessToken: servers[0].accessToken,
556 videoId: video1Server2Id
557 })
558 })
559
560 it('Should have 2 webseeds on the first video', async function () {
561 this.timeout(80000)
562
563 await waitJobs(servers)
564 await waitUntilLog(servers[0], 'Duplicated ', 5)
565 await waitJobs(servers)
566
567 await check2Webseeds()
568 await check1PlaylistRedundancies()
569 await checkStatsWith1Redundancy('manual')
570 })
571
572 it('Should manually remove redundancies on server 1 and remove duplicated videos', async function () {
573 this.timeout(80000)
574
575 const res = await listVideoRedundancies({
576 url: servers[0].url,
577 accessToken: servers[0].accessToken,
578 target: 'remote-videos'
579 })
580
581 const videos = res.body.data as VideoRedundancy[]
582 expect(videos).to.have.lengthOf(1)
583
584 const video = videos[0]
585 for (const r of video.redundancies.files.concat(video.redundancies.streamingPlaylists)) {
586 await removeVideoRedundancy({
587 url: servers[0].url,
588 accessToken: servers[0].accessToken,
589 redundancyId: r.id
590 })
591 }
592
593 await waitJobs(servers)
594 await wait(5000)
595
596 await check1WebSeed()
597 await check0PlaylistRedundancies()
598
599 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ])
600 })
601
602 after(async function () {
603 await cleanupTests(servers)
604 })
605 })
606
607 describe('Test expiration', function () {
608 const strategy = 'recently-added'
609
610 async function checkContains (servers: ServerInfo[], str: string) {
611 for (const server of servers) {
612 const res = await getVideo(server.url, video1Server2UUID)
613 const video: VideoDetails = res.body
614
615 for (const f of video.files) {
616 expect(f.magnetUri).to.contain(str)
617 }
618 }
619 }
620
621 async function checkNotContains (servers: ServerInfo[], str: string) {
622 for (const server of servers) {
623 const res = await getVideo(server.url, video1Server2UUID)
624 const video: VideoDetails = res.body
625
626 for (const f of video.files) {
627 expect(f.magnetUri).to.not.contain(str)
628 }
629 }
630 }
631
632 before(async function () {
633 this.timeout(120000)
634
635 await flushAndRunServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
636
637 await enableRedundancyOnServer1()
638 })
639
640 it('Should still have 2 webseeds after 10 seconds', async function () {
641 this.timeout(80000)
642
643 await wait(10000)
644
645 try {
646 await checkContains(servers, 'http%3A%2F%2Flocalhost%3A' + servers[0].port)
647 } catch {
648 // Maybe a server deleted a redundancy in the scheduler
649 await wait(2000)
650
651 await checkContains(servers, 'http%3A%2F%2Flocalhost%3A' + servers[0].port)
652 }
653 })
654
655 it('Should stop server 1 and expire video redundancy', async function () {
656 this.timeout(80000)
657
658 killallServers([ servers[0] ])
659
660 await wait(15000)
661
662 await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A' + servers[0].port)
663 })
664
665 after(async function () {
666 await cleanupTests(servers)
667 })
668 })
669
670 describe('Test file replacement', function () {
671 let video2Server2UUID: string
672 const strategy = 'recently-added'
673
674 before(async function () {
675 this.timeout(120000)
676
677 await flushAndRunServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
678
679 await enableRedundancyOnServer1()
680
681 await waitJobs(servers)
682 await waitUntilLog(servers[0], 'Duplicated ', 5)
683 await waitJobs(servers)
684
685 await check2Webseeds()
686 await check1PlaylistRedundancies()
687 await checkStatsWith1Redundancy(strategy)
688
689 const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video 2 server 2' })
690 video2Server2UUID = res.body.video.uuid
691 })
692
693 it('Should cache video 2 webseeds on the first video', async function () {
694 this.timeout(120000)
695
696 await waitJobs(servers)
697
698 let checked = false
699
700 while (checked === false) {
701 await wait(1000)
702
703 try {
704 await check1WebSeed(video1Server2UUID)
705 await check0PlaylistRedundancies(video1Server2UUID)
706 await check2Webseeds(video2Server2UUID)
707 await check1PlaylistRedundancies(video2Server2UUID)
708
709 checked = true
710 } catch {
711 checked = false
712 }
713 }
714 })
715
716 it('Should disable strategy and remove redundancies', async function () {
717 this.timeout(80000)
718
719 await waitJobs(servers)
720
721 killallServers([ servers[0] ])
722 await reRunServer(servers[0], {
723 redundancy: {
724 videos: {
725 check_interval: '1 second',
726 strategies: []
727 }
728 }
729 })
730
731 await waitJobs(servers)
732
733 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].internalServerNumber, [ join('redundancy', 'hls') ])
734 })
735
736 after(async function () {
737 await cleanupTests(servers)
738 })
739 })
740})