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