Merge branch 'hotfix/docker' into develop
[oweals/peertube.git] / server / tests / api / redundancy / redundancy.ts
1 /* tslint:disable:no-unused-expression */
2
3 import * as chai from 'chai'
4 import 'mocha'
5 import { VideoDetails } from '../../../../shared/models/videos'
6 import {
7   doubleFollow,
8   flushAndRunMultipleServers,
9   getFollowingListPaginationAndSort,
10   getVideo,
11   immutableAssign,
12   killallServers, makeGetRequest,
13   root,
14   ServerInfo,
15   setAccessTokensToServers, unfollow,
16   uploadVideo,
17   viewVideo,
18   wait,
19   waitUntilLog,
20   checkVideoFilesWereRemoved, removeVideo, getVideoWithToken
21 } from '../../utils'
22 import { waitJobs } from '../../utils/server/jobs'
23 import * as magnetUtil from 'magnet-uri'
24 import { updateRedundancy } from '../../utils/server/redundancy'
25 import { ActorFollow } from '../../../../shared/models/actors'
26 import { readdir } from 'fs-extra'
27 import { join } from 'path'
28 import { VideoRedundancyStrategy } from '../../../../shared/models/redundancy'
29 import { getStats } from '../../utils/server/stats'
30 import { ServerStats } from '../../../../shared/models/server/server-stats.model'
31
32 const expect = chai.expect
33
34 let servers: ServerInfo[] = []
35 let video1Server2UUID: string
36
37 function checkMagnetWebseeds (file: { magnetUri: string, resolution: { id: number } }, baseWebseeds: string[], server: ServerInfo) {
38   const parsed = magnetUtil.decode(file.magnetUri)
39
40   for (const ws of baseWebseeds) {
41     const found = parsed.urlList.find(url => url === `${ws}-${file.resolution.id}.mp4`)
42     expect(found, `Webseed ${ws} not found in ${file.magnetUri} on server ${server.url}`).to.not.be.undefined
43   }
44
45   expect(parsed.urlList).to.have.lengthOf(baseWebseeds.length)
46 }
47
48 async function runServers (strategy: VideoRedundancyStrategy, additionalParams: any = {}) {
49   const config = {
50     redundancy: {
51       videos: {
52         check_interval: '5 seconds',
53         strategies: [
54           immutableAssign({
55             min_lifetime: '1 hour',
56             strategy: strategy,
57             size: '200KB'
58           }, additionalParams)
59         ]
60       }
61     }
62   }
63   servers = await flushAndRunMultipleServers(3, config)
64
65   // Get the access tokens
66   await setAccessTokensToServers(servers)
67
68   {
69     const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 1 server 2' })
70     video1Server2UUID = res.body.video.uuid
71
72     await viewVideo(servers[ 1 ].url, video1Server2UUID)
73   }
74
75   await waitJobs(servers)
76
77   // Server 1 and server 2 follow each other
78   await doubleFollow(servers[ 0 ], servers[ 1 ])
79   // Server 1 and server 3 follow each other
80   await doubleFollow(servers[ 0 ], servers[ 2 ])
81   // Server 2 and server 3 follow each other
82   await doubleFollow(servers[ 1 ], servers[ 2 ])
83
84   await waitJobs(servers)
85 }
86
87 async function check1WebSeed (strategy: VideoRedundancyStrategy, videoUUID?: string) {
88   if (!videoUUID) videoUUID = video1Server2UUID
89
90   const webseeds = [
91     'http://localhost:9002/static/webseed/' + videoUUID
92   ]
93
94   for (const server of servers) {
95     {
96       // With token to avoid issues with video follow constraints
97       const res = await getVideoWithToken(server.url, server.accessToken, videoUUID)
98
99       const video: VideoDetails = res.body
100       for (const f of video.files) {
101         checkMagnetWebseeds(f, webseeds, server)
102       }
103     }
104   }
105 }
106
107 async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) {
108   const res = await getStats(servers[0].url)
109   const data: ServerStats = res.body
110
111   expect(data.videosRedundancy).to.have.lengthOf(1)
112   const stat = data.videosRedundancy[0]
113
114   expect(stat.strategy).to.equal(strategy)
115   expect(stat.totalSize).to.equal(204800)
116   expect(stat.totalUsed).to.be.at.least(1).and.below(204801)
117   expect(stat.totalVideoFiles).to.equal(4)
118   expect(stat.totalVideos).to.equal(1)
119 }
120
121 async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategy) {
122   const res = await getStats(servers[0].url)
123   const data: ServerStats = res.body
124
125   expect(data.videosRedundancy).to.have.lengthOf(1)
126
127   const stat = data.videosRedundancy[0]
128   expect(stat.strategy).to.equal(strategy)
129   expect(stat.totalSize).to.equal(204800)
130   expect(stat.totalUsed).to.equal(0)
131   expect(stat.totalVideoFiles).to.equal(0)
132   expect(stat.totalVideos).to.equal(0)
133 }
134
135 async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: string) {
136   if (!videoUUID) videoUUID = video1Server2UUID
137
138   const webseeds = [
139     'http://localhost:9001/static/redundancy/' + videoUUID,
140     'http://localhost:9002/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: 200,
154         path: '/static/redundancy/' + `${videoUUID}-${file.resolution.id}.mp4`,
155         contentType: null
156       })
157       await makeGetRequest({
158         url: servers[1].url,
159         statusCodeExpected: 200,
160         path: '/static/webseed/' + `${videoUUID}-${file.resolution.id}.mp4`,
161         contentType: null
162       })
163     }
164   }
165
166   for (const directory of [ 'test1/redundancy', 'test2/videos' ]) {
167     const files = await readdir(join(root(), directory))
168     expect(files).to.have.length.at.least(4)
169
170     for (const resolution of [ 240, 360, 480, 720 ]) {
171       expect(files.find(f => f === `${videoUUID}-${resolution}.mp4`)).to.not.be.undefined
172     }
173   }
174 }
175
176 async function enableRedundancyOnServer1 () {
177   await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, true)
178
179   const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 5, '-createdAt')
180   const follows: ActorFollow[] = res.body.data
181   const server2 = follows.find(f => f.following.host === 'localhost:9002')
182   const server3 = follows.find(f => f.following.host === 'localhost:9003')
183
184   expect(server3).to.not.be.undefined
185   expect(server3.following.hostRedundancyAllowed).to.be.false
186
187   expect(server2).to.not.be.undefined
188   expect(server2.following.hostRedundancyAllowed).to.be.true
189 }
190
191 async function disableRedundancyOnServer1 () {
192   await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, false)
193
194   const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 5, '-createdAt')
195   const follows: ActorFollow[] = res.body.data
196   const server2 = follows.find(f => f.following.host === 'localhost:9002')
197   const server3 = follows.find(f => f.following.host === 'localhost:9003')
198
199   expect(server3).to.not.be.undefined
200   expect(server3.following.hostRedundancyAllowed).to.be.false
201
202   expect(server2).to.not.be.undefined
203   expect(server2.following.hostRedundancyAllowed).to.be.false
204 }
205
206 async function cleanServers () {
207   killallServers(servers)
208 }
209
210 describe('Test videos redundancy', function () {
211
212   describe('With most-views strategy', function () {
213     const strategy = 'most-views'
214
215     before(function () {
216       this.timeout(120000)
217
218       return runServers(strategy)
219     })
220
221     it('Should have 1 webseed on the first video', async function () {
222       await check1WebSeed(strategy)
223       await checkStatsWith1Webseed(strategy)
224     })
225
226     it('Should enable redundancy on server 1', function () {
227       return enableRedundancyOnServer1()
228     })
229
230     it('Should have 2 webseeds on the first video', async function () {
231       this.timeout(40000)
232
233       await waitJobs(servers)
234       await waitUntilLog(servers[0], 'Duplicated ', 4)
235       await waitJobs(servers)
236
237       await check2Webseeds(strategy)
238       await checkStatsWith2Webseed(strategy)
239     })
240
241     it('Should undo redundancy on server 1 and remove duplicated videos', async function () {
242       this.timeout(40000)
243
244       await disableRedundancyOnServer1()
245
246       await waitJobs(servers)
247       await wait(5000)
248
249       await check1WebSeed(strategy)
250
251       await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ])
252     })
253
254     after(function () {
255       return cleanServers()
256     })
257   })
258
259   describe('With trending strategy', function () {
260     const strategy = 'trending'
261
262     before(function () {
263       this.timeout(120000)
264
265       return runServers(strategy)
266     })
267
268     it('Should have 1 webseed on the first video', async function () {
269       await check1WebSeed(strategy)
270       await checkStatsWith1Webseed(strategy)
271     })
272
273     it('Should enable redundancy on server 1', function () {
274       return enableRedundancyOnServer1()
275     })
276
277     it('Should have 2 webseeds on the first video', async function () {
278       this.timeout(40000)
279
280       await waitJobs(servers)
281       await waitUntilLog(servers[0], 'Duplicated ', 4)
282       await waitJobs(servers)
283
284       await check2Webseeds(strategy)
285       await checkStatsWith2Webseed(strategy)
286     })
287
288     it('Should unfollow on server 1 and remove duplicated videos', async function () {
289       this.timeout(40000)
290
291       await unfollow(servers[0].url, servers[0].accessToken, servers[1])
292
293       await waitJobs(servers)
294       await wait(5000)
295
296       await check1WebSeed(strategy)
297
298       await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ])
299     })
300
301     after(function () {
302       return cleanServers()
303     })
304   })
305
306   describe('With recently added strategy', function () {
307     const strategy = 'recently-added'
308
309     before(function () {
310       this.timeout(120000)
311
312       return runServers(strategy, { min_views: 3 })
313     })
314
315     it('Should have 1 webseed on the first video', async function () {
316       await check1WebSeed(strategy)
317       await checkStatsWith1Webseed(strategy)
318     })
319
320     it('Should enable redundancy on server 1', function () {
321       return enableRedundancyOnServer1()
322     })
323
324     it('Should still have 1 webseed on the first video', async function () {
325       this.timeout(40000)
326
327       await waitJobs(servers)
328       await wait(15000)
329       await waitJobs(servers)
330
331       await check1WebSeed(strategy)
332       await checkStatsWith1Webseed(strategy)
333     })
334
335     it('Should view 2 times the first video to have > min_views config', async function () {
336       this.timeout(40000)
337
338       await viewVideo(servers[ 0 ].url, video1Server2UUID)
339       await viewVideo(servers[ 2 ].url, video1Server2UUID)
340
341       await wait(10000)
342       await waitJobs(servers)
343     })
344
345     it('Should have 2 webseeds on the first video', async function () {
346       this.timeout(40000)
347
348       await waitJobs(servers)
349       await waitUntilLog(servers[0], 'Duplicated ', 4)
350       await waitJobs(servers)
351
352       await check2Webseeds(strategy)
353       await checkStatsWith2Webseed(strategy)
354     })
355
356     it('Should remove the video and the redundancy files', async function () {
357       this.timeout(20000)
358
359       await removeVideo(servers[1].url, servers[1].accessToken, video1Server2UUID)
360
361       await waitJobs(servers)
362
363       for (const server of servers) {
364         await checkVideoFilesWereRemoved(video1Server2UUID, server.serverNumber)
365       }
366     })
367
368     after(function () {
369       return cleanServers()
370     })
371   })
372
373   describe('Test expiration', function () {
374     const strategy = 'recently-added'
375
376     async function checkContains (servers: ServerInfo[], str: string) {
377       for (const server of servers) {
378         const res = await getVideo(server.url, video1Server2UUID)
379         const video: VideoDetails = res.body
380
381         for (const f of video.files) {
382           expect(f.magnetUri).to.contain(str)
383         }
384       }
385     }
386
387     async function checkNotContains (servers: ServerInfo[], str: string) {
388       for (const server of servers) {
389         const res = await getVideo(server.url, video1Server2UUID)
390         const video: VideoDetails = res.body
391
392         for (const f of video.files) {
393           expect(f.magnetUri).to.not.contain(str)
394         }
395       }
396     }
397
398     before(async function () {
399       this.timeout(120000)
400
401       await runServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
402
403       await enableRedundancyOnServer1()
404     })
405
406     it('Should still have 2 webseeds after 10 seconds', async function () {
407       this.timeout(40000)
408
409       await wait(10000)
410
411       try {
412         await checkContains(servers, 'http%3A%2F%2Flocalhost%3A9001')
413       } catch {
414         // Maybe a server deleted a redundancy in the scheduler
415         await wait(2000)
416
417         await checkContains(servers, 'http%3A%2F%2Flocalhost%3A9001')
418       }
419     })
420
421     it('Should stop server 1 and expire video redundancy', async function () {
422       this.timeout(40000)
423
424       killallServers([ servers[0] ])
425
426       await wait(15000)
427
428       await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001')
429     })
430
431     after(function () {
432       return killallServers([ servers[1], servers[2] ])
433     })
434   })
435
436   describe('Test file replacement', function () {
437     let video2Server2UUID: string
438     const strategy = 'recently-added'
439
440     before(async function () {
441       this.timeout(120000)
442
443       await runServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
444
445       await enableRedundancyOnServer1()
446
447       await waitJobs(servers)
448       await waitUntilLog(servers[0], 'Duplicated ', 4)
449       await waitJobs(servers)
450
451       await check2Webseeds(strategy)
452       await checkStatsWith2Webseed(strategy)
453
454       const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 2 server 2' })
455       video2Server2UUID = res.body.video.uuid
456     })
457
458     it('Should cache video 2 webseeds on the first video', async function () {
459       this.timeout(120000)
460
461       await waitJobs(servers)
462
463       let checked = false
464
465       while (checked === false) {
466         await wait(1000)
467
468         try {
469           await check1WebSeed(strategy, video1Server2UUID)
470           await check2Webseeds(strategy, video2Server2UUID)
471
472           checked = true
473         } catch {
474           checked = false
475         }
476       }
477     })
478
479     after(function () {
480       return cleanServers()
481     })
482   })
483 })