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