Optimize actor follow scores modifications

pull/1535/head
Chocobozzz 2018-12-20 14:31:11 +01:00
parent 4707f410ae
commit 2f5c6b2fc6
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
14 changed files with 118 additions and 73 deletions

View File

@ -94,7 +94,7 @@ import {
} from './server/controllers' } from './server/controllers'
import { advertiseDoNotTrack } from './server/middlewares/dnt' import { advertiseDoNotTrack } from './server/middlewares/dnt'
import { Redis } from './server/lib/redis' import { Redis } from './server/lib/redis'
import { BadActorFollowScheduler } from './server/lib/schedulers/bad-actor-follow-scheduler' import { ActorFollowScheduler } from './server/lib/schedulers/actor-follow-scheduler'
import { RemoveOldJobsScheduler } from './server/lib/schedulers/remove-old-jobs-scheduler' import { RemoveOldJobsScheduler } from './server/lib/schedulers/remove-old-jobs-scheduler'
import { UpdateVideosScheduler } from './server/lib/schedulers/update-videos-scheduler' import { UpdateVideosScheduler } from './server/lib/schedulers/update-videos-scheduler'
import { YoutubeDlUpdateScheduler } from './server/lib/schedulers/youtube-dl-update-scheduler' import { YoutubeDlUpdateScheduler } from './server/lib/schedulers/youtube-dl-update-scheduler'
@ -219,7 +219,7 @@ async function startApplication () {
VideosCaptionCache.Instance.init(CONFIG.CACHE.VIDEO_CAPTIONS.SIZE, CACHE.VIDEO_CAPTIONS.MAX_AGE) VideosCaptionCache.Instance.init(CONFIG.CACHE.VIDEO_CAPTIONS.SIZE, CACHE.VIDEO_CAPTIONS.MAX_AGE)
// Enable Schedulers // Enable Schedulers
BadActorFollowScheduler.Instance.enable() ActorFollowScheduler.Instance.enable()
RemoveOldJobsScheduler.Instance.enable() RemoveOldJobsScheduler.Instance.enable()
UpdateVideosScheduler.Instance.enable() UpdateVideosScheduler.Instance.enable()
YoutubeDlUpdateScheduler.Instance.enable() YoutubeDlUpdateScheduler.Instance.enable()

View File

@ -144,7 +144,7 @@ const VIDEO_IMPORT_TIMEOUT = 1000 * 3600 // 1 hour
// 1 hour // 1 hour
let SCHEDULER_INTERVALS_MS = { let SCHEDULER_INTERVALS_MS = {
badActorFollow: 60000 * 60, // 1 hour actorFollowScores: 60000 * 60, // 1 hour
removeOldJobs: 60000 * 60, // 1 hour removeOldJobs: 60000 * 60, // 1 hour
updateVideos: 60000, // 1 minute updateVideos: 60000, // 1 minute
youtubeDLUpdate: 60000 * 60 * 24 // 1 day youtubeDLUpdate: 60000 * 60 * 24 // 1 day
@ -675,7 +675,7 @@ if (isTestInstance() === true) {
CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB
SCHEDULER_INTERVALS_MS.badActorFollow = 10000 SCHEDULER_INTERVALS_MS.actorFollowScores = 1000
SCHEDULER_INTERVALS_MS.removeOldJobs = 10000 SCHEDULER_INTERVALS_MS.removeOldJobs = 10000
SCHEDULER_INTERVALS_MS.updateVideos = 5000 SCHEDULER_INTERVALS_MS.updateVideos = 5000
REPEAT_JOBS['videos-views'] = { every: 5000 } REPEAT_JOBS['videos-views'] = { every: 5000 }

View File

@ -0,0 +1,46 @@
import { ACTOR_FOLLOW_SCORE } from '../../initializers'
import { logger } from '../../helpers/logger'
// Cache follows scores, instead of writing them too often in database
// Keep data in memory, we don't really need Redis here as we don't really care to loose some scores
class ActorFollowScoreCache {
private static instance: ActorFollowScoreCache
private pendingFollowsScore: { [ url: string ]: number } = {}
private constructor () {}
static get Instance () {
return this.instance || (this.instance = new this())
}
updateActorFollowsScore (goodInboxes: string[], badInboxes: string[]) {
if (goodInboxes.length === 0 && badInboxes.length === 0) return
logger.info('Updating %d good actor follows and %d bad actor follows scores in cache.', goodInboxes.length, badInboxes.length)
for (const goodInbox of goodInboxes) {
if (this.pendingFollowsScore[goodInbox] === undefined) this.pendingFollowsScore[goodInbox] = 0
this.pendingFollowsScore[goodInbox] += ACTOR_FOLLOW_SCORE.BONUS
}
for (const badInbox of badInboxes) {
if (this.pendingFollowsScore[badInbox] === undefined) this.pendingFollowsScore[badInbox] = 0
this.pendingFollowsScore[badInbox] += ACTOR_FOLLOW_SCORE.PENALTY
}
}
getPendingFollowsScoreCopy () {
return this.pendingFollowsScore
}
clearPendingFollowsScore () {
this.pendingFollowsScore = {}
}
}
export {
ActorFollowScoreCache
}

View File

@ -1,2 +1,3 @@
export * from './actor-follow-score-cache'
export * from './videos-preview-cache' export * from './videos-preview-cache'
export * from './videos-caption-cache' export * from './videos-caption-cache'

View File

@ -5,6 +5,7 @@ import { doRequest } from '../../../helpers/requests'
import { ActorFollowModel } from '../../../models/activitypub/actor-follow' import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils'
import { BROADCAST_CONCURRENCY, JOB_REQUEST_TIMEOUT } from '../../../initializers' import { BROADCAST_CONCURRENCY, JOB_REQUEST_TIMEOUT } from '../../../initializers'
import { ActorFollowScoreCache } from '../../cache'
export type ActivitypubHttpBroadcastPayload = { export type ActivitypubHttpBroadcastPayload = {
uris: string[] uris: string[]
@ -38,7 +39,7 @@ async function processActivityPubHttpBroadcast (job: Bull.Job) {
.catch(() => badUrls.push(uri)) .catch(() => badUrls.push(uri))
}, { concurrency: BROADCAST_CONCURRENCY }) }, { concurrency: BROADCAST_CONCURRENCY })
return ActorFollowModel.updateActorFollowsScore(goodUrls, badUrls, undefined) return ActorFollowScoreCache.Instance.updateActorFollowsScore(goodUrls, badUrls)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -1,9 +1,9 @@
import * as Bull from 'bull' import * as Bull from 'bull'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { doRequest } from '../../../helpers/requests' import { doRequest } from '../../../helpers/requests'
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils'
import { JOB_REQUEST_TIMEOUT } from '../../../initializers' import { JOB_REQUEST_TIMEOUT } from '../../../initializers'
import { ActorFollowScoreCache } from '../../cache'
export type ActivitypubHttpUnicastPayload = { export type ActivitypubHttpUnicastPayload = {
uri: string uri: string
@ -31,9 +31,9 @@ async function processActivityPubHttpUnicast (job: Bull.Job) {
try { try {
await doRequest(options) await doRequest(options)
ActorFollowModel.updateActorFollowsScore([ uri ], [], undefined) ActorFollowScoreCache.Instance.updateActorFollowsScore([ uri ], [])
} catch (err) { } catch (err) {
ActorFollowModel.updateActorFollowsScore([], [ uri ], undefined) ActorFollowScoreCache.Instance.updateActorFollowsScore([], [ uri ])
throw err throw err
} }

View File

@ -165,10 +165,10 @@ class JobQueue {
return total return total
} }
removeOldJobs () { async removeOldJobs () {
for (const key of Object.keys(this.queues)) { for (const key of Object.keys(this.queues)) {
const queue = this.queues[key] const queue = this.queues[key]
queue.clean(JOB_COMPLETED_LIFETIME, 'completed') await queue.clean(JOB_COMPLETED_LIFETIME, 'completed')
} }
} }

View File

@ -1,8 +1,11 @@
import { logger } from '../../helpers/logger'
export abstract class AbstractScheduler { export abstract class AbstractScheduler {
protected abstract schedulerIntervalMs: number protected abstract schedulerIntervalMs: number
private interval: NodeJS.Timer private interval: NodeJS.Timer
private isRunning = false
enable () { enable () {
if (!this.schedulerIntervalMs) throw new Error('Interval is not correctly set.') if (!this.schedulerIntervalMs) throw new Error('Interval is not correctly set.')
@ -14,5 +17,18 @@ export abstract class AbstractScheduler {
clearInterval(this.interval) clearInterval(this.interval)
} }
abstract execute () async execute () {
if (this.isRunning === true) return
this.isRunning = true
try {
await this.internalExecute()
} catch (err) {
logger.error('Cannot execute %s scheduler.', this.constructor.name, { err })
} finally {
this.isRunning = false
}
}
protected abstract internalExecute (): Promise<any>
} }

View File

@ -3,18 +3,35 @@ import { logger } from '../../helpers/logger'
import { ActorFollowModel } from '../../models/activitypub/actor-follow' import { ActorFollowModel } from '../../models/activitypub/actor-follow'
import { AbstractScheduler } from './abstract-scheduler' import { AbstractScheduler } from './abstract-scheduler'
import { SCHEDULER_INTERVALS_MS } from '../../initializers' import { SCHEDULER_INTERVALS_MS } from '../../initializers'
import { ActorFollowScoreCache } from '../cache'
export class BadActorFollowScheduler extends AbstractScheduler { export class ActorFollowScheduler extends AbstractScheduler {
private static instance: AbstractScheduler private static instance: AbstractScheduler
protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.badActorFollow protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.actorFollowScores
private constructor () { private constructor () {
super() super()
} }
async execute () { protected async internalExecute () {
await this.processPendingScores()
await this.removeBadActorFollows()
}
private async processPendingScores () {
const pendingScores = ActorFollowScoreCache.Instance.getPendingFollowsScoreCopy()
ActorFollowScoreCache.Instance.clearPendingFollowsScore()
for (const inbox of Object.keys(pendingScores)) {
await ActorFollowModel.updateFollowScore(inbox, pendingScores[inbox])
}
}
private async removeBadActorFollows () {
if (!isTestInstance()) logger.info('Removing bad actor follows (scheduler).') if (!isTestInstance()) logger.info('Removing bad actor follows (scheduler).')
try { try {

View File

@ -14,10 +14,10 @@ export class RemoveOldJobsScheduler extends AbstractScheduler {
super() super()
} }
async execute () { protected internalExecute () {
if (!isTestInstance()) logger.info('Removing old jobs (scheduler).') if (!isTestInstance()) logger.info('Removing old jobs in scheduler.')
JobQueue.Instance.removeOldJobs() return JobQueue.Instance.removeOldJobs()
} }
static get Instance () { static get Instance () {

View File

@ -12,23 +12,12 @@ export class UpdateVideosScheduler extends AbstractScheduler {
protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.updateVideos protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.updateVideos
private isRunning = false
private constructor () { private constructor () {
super() super()
} }
async execute () { protected async internalExecute () {
if (this.isRunning === true) return return retryTransactionWrapper(this.updateVideos.bind(this))
this.isRunning = true
try {
await retryTransactionWrapper(this.updateVideos.bind(this))
} catch (err) {
logger.error('Cannot execute update videos scheduler.', { err })
} finally {
this.isRunning = false
}
} }
private async updateVideos () { private async updateVideos () {

View File

@ -16,7 +16,6 @@ import { getOrCreateVideoAndAccountAndChannel } from '../activitypub'
export class VideosRedundancyScheduler extends AbstractScheduler { export class VideosRedundancyScheduler extends AbstractScheduler {
private static instance: AbstractScheduler private static instance: AbstractScheduler
private executing = false
protected schedulerIntervalMs = CONFIG.REDUNDANCY.VIDEOS.CHECK_INTERVAL protected schedulerIntervalMs = CONFIG.REDUNDANCY.VIDEOS.CHECK_INTERVAL
@ -24,11 +23,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
super() super()
} }
async execute () { protected async internalExecute () {
if (this.executing) return
this.executing = true
for (const obj of CONFIG.REDUNDANCY.VIDEOS.STRATEGIES) { for (const obj of CONFIG.REDUNDANCY.VIDEOS.STRATEGIES) {
logger.info('Running redundancy scheduler for strategy %s.', obj.strategy) logger.info('Running redundancy scheduler for strategy %s.', obj.strategy)
@ -57,8 +52,6 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
await this.extendsLocalExpiration() await this.extendsLocalExpiration()
await this.purgeRemoteExpired() await this.purgeRemoteExpired()
this.executing = false
} }
static get Instance () { static get Instance () {

View File

@ -12,7 +12,7 @@ export class YoutubeDlUpdateScheduler extends AbstractScheduler {
super() super()
} }
execute () { protected internalExecute () {
return updateYoutubeDLBinary() return updateYoutubeDLBinary()
} }

View File

@ -127,22 +127,6 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved) if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved)
} }
static updateActorFollowsScore (goodInboxes: string[], badInboxes: string[], t: Sequelize.Transaction | undefined) {
if (goodInboxes.length === 0 && badInboxes.length === 0) return
logger.info('Updating %d good actor follows and %d bad actor follows scores.', goodInboxes.length, badInboxes.length)
if (goodInboxes.length !== 0) {
ActorFollowModel.incrementScores(goodInboxes, ACTOR_FOLLOW_SCORE.BONUS, t)
.catch(err => logger.error('Cannot increment scores of good actor follows.', { err }))
}
if (badInboxes.length !== 0) {
ActorFollowModel.incrementScores(badInboxes, ACTOR_FOLLOW_SCORE.PENALTY, t)
.catch(err => logger.error('Cannot decrement scores of bad actor follows.', { err }))
}
}
static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) { static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) {
const query = { const query = {
where: { where: {
@ -464,6 +448,22 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
} }
} }
static updateFollowScore (inboxUrl: string, value: number, t?: Sequelize.Transaction) {
const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
'WHERE id IN (' +
'SELECT "actorFollow"."id" FROM "actorFollow" ' +
'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
`WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` +
')'
const options = {
type: Sequelize.QueryTypes.BULKUPDATE,
transaction: t
}
return ActorFollowModel.sequelize.query(query, options)
}
private static async createListAcceptedFollowForApiQuery ( private static async createListAcceptedFollowForApiQuery (
type: 'followers' | 'following', type: 'followers' | 'following',
actorIds: number[], actorIds: number[],
@ -518,24 +518,6 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
} }
} }
private static incrementScores (inboxUrls: string[], value: number, t: Sequelize.Transaction | undefined) {
const inboxUrlsString = inboxUrls.map(url => `'${url}'`).join(',')
const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
'WHERE id IN (' +
'SELECT "actorFollow"."id" FROM "actorFollow" ' +
'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
'WHERE "actor"."inboxUrl" IN (' + inboxUrlsString + ') OR "actor"."sharedInboxUrl" IN (' + inboxUrlsString + ')' +
')'
const options = t ? {
type: Sequelize.QueryTypes.BULKUPDATE,
transaction: t
} : undefined
return ActorFollowModel.sequelize.query(query, options)
}
private static listBadActorFollows () { private static listBadActorFollows () {
const query = { const query = {
where: { where: {