Merge branch 'feature/webtorrent-disabling' 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
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: '100KB'
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       const res = await getVideo(server.url, videoUUID)
97
98       const video: VideoDetails = res.body
99       for (const f of video.files) {
100         checkMagnetWebseeds(f, webseeds, server)
101       }
102     }
103   }
104 }
105
106 async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) {
107   const res = await getStats(servers[0].url)
108   const data: ServerStats = res.body
109
110   expect(data.videosRedundancy).to.have.lengthOf(1)
111   const stat = data.videosRedundancy[0]
112
113   expect(stat.strategy).to.equal(strategy)
114   expect(stat.totalSize).to.equal(102400)
115   expect(stat.totalUsed).to.be.at.least(1).and.below(102401)
116   expect(stat.totalVideoFiles).to.equal(4)
117   expect(stat.totalVideos).to.equal(1)
118 }
119
120 async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategy) {
121   const res = await getStats(servers[0].url)
122   const data: ServerStats = res.body
123
124   expect(data.videosRedundancy).to.have.lengthOf(1)
125
126   const stat = data.videosRedundancy[0]
127   expect(stat.strategy).to.equal(strategy)
128   expect(stat.totalSize).to.equal(102400)
129   expect(stat.totalUsed).to.equal(0)
130   expect(stat.totalVideoFiles).to.equal(0)
131   expect(stat.totalVideos).to.equal(0)
132 }
133
134 async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: string) {
135   if (!videoUUID) videoUUID = video1Server2UUID
136
137   const webseeds = [
138     'http://localhost:9001/static/webseed/' + videoUUID,
139     'http://localhost:9002/static/webseed/' + videoUUID
140   ]
141
142   for (const server of servers) {
143     const res = await getVideo(server.url, videoUUID)
144
145     const video: VideoDetails = res.body
146
147     for (const file of video.files) {
148       checkMagnetWebseeds(file, webseeds, server)
149
150       // Only servers 1 and 2 have the video
151       if (server.serverNumber !== 3) {
152         await makeGetRequest({
153           url: server.url,
154           statusCodeExpected: 200,
155           path: '/static/webseed/' + `${videoUUID}-${file.resolution.id}.mp4`,
156           contentType: null
157         })
158       }
159     }
160   }
161
162   for (const directory of [ 'test1', 'test2' ]) {
163     const files = await readdir(join(root(), directory, 'videos'))
164     expect(files).to.have.length.at.least(4)
165
166     for (const resolution of [ 240, 360, 480, 720 ]) {
167       expect(files.find(f => f === `${videoUUID}-${resolution}.mp4`)).to.not.be.undefined
168     }
169   }
170 }
171
172 async function enableRedundancyOnServer1 () {
173   await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, true)
174
175   const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 5, '-createdAt')
176   const follows: ActorFollow[] = res.body.data
177   const server2 = follows.find(f => f.following.host === 'localhost:9002')
178   const server3 = follows.find(f => f.following.host === 'localhost:9003')
179
180   expect(server3).to.not.be.undefined
181   expect(server3.following.hostRedundancyAllowed).to.be.false
182
183   expect(server2).to.not.be.undefined
184   expect(server2.following.hostRedundancyAllowed).to.be.true
185 }
186
187 async function disableRedundancyOnServer1 () {
188   await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, false)
189
190   const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 5, '-createdAt')
191   const follows: ActorFollow[] = res.body.data
192   const server2 = follows.find(f => f.following.host === 'localhost:9002')
193   const server3 = follows.find(f => f.following.host === 'localhost:9003')
194
195   expect(server3).to.not.be.undefined
196   expect(server3.following.hostRedundancyAllowed).to.be.false
197
198   expect(server2).to.not.be.undefined
199   expect(server2.following.hostRedundancyAllowed).to.be.false
200 }
201
202 async function cleanServers () {
203   killallServers(servers)
204 }
205
206 describe('Test videos redundancy', function () {
207
208   describe('With most-views strategy', function () {
209     const strategy = 'most-views'
210
211     before(function () {
212       this.timeout(120000)
213
214       return runServers(strategy)
215     })
216
217     it('Should have 1 webseed on the first video', async function () {
218       await check1WebSeed(strategy)
219       await checkStatsWith1Webseed(strategy)
220     })
221
222     it('Should enable redundancy on server 1', function () {
223       return enableRedundancyOnServer1()
224     })
225
226     it('Should have 2 webseed on the first video', async function () {
227       this.timeout(40000)
228
229       await waitJobs(servers)
230       await waitUntilLog(servers[0], 'Duplicated ', 4)
231       await waitJobs(servers)
232
233       await check2Webseeds(strategy)
234       await checkStatsWith2Webseed(strategy)
235     })
236
237     it('Should undo redundancy on server 1 and remove duplicated videos', async function () {
238       this.timeout(40000)
239
240       await disableRedundancyOnServer1()
241
242       await waitJobs(servers)
243       await wait(5000)
244
245       await check1WebSeed(strategy)
246
247       await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ])
248     })
249
250     after(function () {
251       return cleanServers()
252     })
253   })
254
255   describe('With trending strategy', function () {
256     const strategy = 'trending'
257
258     before(function () {
259       this.timeout(120000)
260
261       return runServers(strategy)
262     })
263
264     it('Should have 1 webseed on the first video', async function () {
265       await check1WebSeed(strategy)
266       await checkStatsWith1Webseed(strategy)
267     })
268
269     it('Should enable redundancy on server 1', function () {
270       return enableRedundancyOnServer1()
271     })
272
273     it('Should have 2 webseed on the first video', async function () {
274       this.timeout(40000)
275
276       await waitJobs(servers)
277       await waitUntilLog(servers[0], 'Duplicated ', 4)
278       await waitJobs(servers)
279
280       await check2Webseeds(strategy)
281       await checkStatsWith2Webseed(strategy)
282     })
283
284     it('Should unfollow on server 1 and remove duplicated videos', async function () {
285       this.timeout(40000)
286
287       await unfollow(servers[0].url, servers[0].accessToken, servers[1])
288
289       await waitJobs(servers)
290       await wait(5000)
291
292       await check1WebSeed(strategy)
293
294       await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ])
295     })
296
297     after(function () {
298       return cleanServers()
299     })
300   })
301
302   describe('With recently added strategy', function () {
303     const strategy = 'recently-added'
304
305     before(function () {
306       this.timeout(120000)
307
308       return runServers(strategy, { min_views: 3 })
309     })
310
311     it('Should have 1 webseed on the first video', async function () {
312       await check1WebSeed(strategy)
313       await checkStatsWith1Webseed(strategy)
314     })
315
316     it('Should enable redundancy on server 1', function () {
317       return enableRedundancyOnServer1()
318     })
319
320     it('Should still have 1 webseed on the first video', async function () {
321       this.timeout(40000)
322
323       await waitJobs(servers)
324       await wait(15000)
325       await waitJobs(servers)
326
327       await check1WebSeed(strategy)
328       await checkStatsWith1Webseed(strategy)
329     })
330
331     it('Should view 2 times the first video to have > min_views config', async function () {
332       this.timeout(40000)
333
334       await viewVideo(servers[ 0 ].url, video1Server2UUID)
335       await viewVideo(servers[ 2 ].url, video1Server2UUID)
336
337       await wait(10000)
338       await waitJobs(servers)
339     })
340
341     it('Should have 2 webseed on the first video', async function () {
342       this.timeout(40000)
343
344       await waitJobs(servers)
345       await waitUntilLog(servers[0], 'Duplicated ', 4)
346       await waitJobs(servers)
347
348       await check2Webseeds(strategy)
349       await checkStatsWith2Webseed(strategy)
350     })
351
352     it('Should remove the video and the redundancy files', async function () {
353       this.timeout(20000)
354
355       await removeVideo(servers[1].url, servers[1].accessToken, video1Server2UUID)
356
357       await waitJobs(servers)
358
359       for (const server of servers) {
360         await checkVideoFilesWereRemoved(video1Server2UUID, server.serverNumber)
361       }
362     })
363
364     after(function () {
365       return cleanServers()
366     })
367   })
368
369   describe('Test expiration', function () {
370     const strategy = 'recently-added'
371
372     async function checkContains (servers: ServerInfo[], str: string) {
373       for (const server of servers) {
374         const res = await getVideo(server.url, video1Server2UUID)
375         const video: VideoDetails = res.body
376
377         for (const f of video.files) {
378           expect(f.magnetUri).to.contain(str)
379         }
380       }
381     }
382
383     async function checkNotContains (servers: ServerInfo[], str: string) {
384       for (const server of servers) {
385         const res = await getVideo(server.url, video1Server2UUID)
386         const video: VideoDetails = res.body
387
388         for (const f of video.files) {
389           expect(f.magnetUri).to.not.contain(str)
390         }
391       }
392     }
393
394     before(async function () {
395       this.timeout(120000)
396
397       await runServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
398
399       await enableRedundancyOnServer1()
400     })
401
402     it('Should still have 2 webseeds after 10 seconds', async function () {
403       this.timeout(40000)
404
405       await wait(10000)
406
407       try {
408         await checkContains(servers, 'http%3A%2F%2Flocalhost%3A9001')
409       } catch {
410         // Maybe a server deleted a redundancy in the scheduler
411         await wait(2000)
412
413         await checkContains(servers, 'http%3A%2F%2Flocalhost%3A9001')
414       }
415     })
416
417     it('Should stop server 1 and expire video redundancy', async function () {
418       this.timeout(40000)
419
420       killallServers([ servers[0] ])
421
422       await wait(10000)
423
424       await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001')
425     })
426
427     after(function () {
428       return killallServers([ servers[1], servers[2] ])
429     })
430   })
431
432   describe('Test file replacement', function () {
433     let video2Server2UUID: string
434     const strategy = 'recently-added'
435
436     before(async function () {
437       this.timeout(120000)
438
439       await runServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
440
441       await enableRedundancyOnServer1()
442
443       await waitJobs(servers)
444       await waitUntilLog(servers[0], 'Duplicated ', 4)
445       await waitJobs(servers)
446
447       await check2Webseeds(strategy)
448       await checkStatsWith2Webseed(strategy)
449
450       const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 2 server 2' })
451       video2Server2UUID = res.body.video.uuid
452     })
453
454     it('Should cache video 2 webseed on the first video', async function () {
455       this.timeout(50000)
456
457       await waitJobs(servers)
458
459       await wait(7000)
460
461       try {
462         await check1WebSeed(strategy, video1Server2UUID)
463         await check2Webseeds(strategy, video2Server2UUID)
464       } catch {
465         await wait(3000)
466
467         try {
468           await check1WebSeed(strategy, video1Server2UUID)
469           await check2Webseeds(strategy, video2Server2UUID)
470         } catch {
471           await wait(5000)
472
473           await check1WebSeed(strategy, video1Server2UUID)
474           await check2Webseeds(strategy, video2Server2UUID)
475         }
476       }
477     })
478
479     after(function () {
480       return cleanServers()
481     })
482   })
483 })