diff options
author | Chocobozzz <me@florianbigard.com> | 2018-04-18 15:32:40 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-04-18 15:32:40 +0200 |
commit | 5350fd8e5b2b2d017b16d97828893a8a4a40bd89 (patch) | |
tree | eddc20eeb19e1501ead0ad72b2fdaffc92708478 | |
parent | bf6e8e3e3dd6516a6125c9c2c12587e7bac8c107 (diff) | |
download | PeerTube-5350fd8e5b2b2d017b16d97828893a8a4a40bd89.tar.gz PeerTube-5350fd8e5b2b2d017b16d97828893a8a4a40bd89.tar.zst PeerTube-5350fd8e5b2b2d017b16d97828893a8a4a40bd89.zip |
Move server follow in the job queue
It helps to track follow errors
-rw-r--r-- | server/controllers/api/server/follows.ts | 78 | ||||
-rw-r--r-- | server/initializers/constants.ts | 2 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/activitypub-follow.ts | 68 | ||||
-rw-r--r-- | server/lib/job-queue/job-queue.ts | 5 | ||||
-rw-r--r-- | shared/models/server/job.model.ts | 1 |
5 files changed, 90 insertions, 64 deletions
diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts index bb0063473..e78361c9a 100644 --- a/server/controllers/api/server/follows.ts +++ b/server/controllers/api/server/follows.ts | |||
@@ -1,20 +1,22 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { UserRight } from '../../../../shared/models/users' | 2 | import { UserRight } from '../../../../shared/models/users' |
3 | import { sanitizeHost } from '../../../helpers/core-utils' | ||
4 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
5 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
6 | import { getFormattedObjects, getServerActor } from '../../../helpers/utils' | 4 | import { getFormattedObjects, getServerActor } from '../../../helpers/utils' |
7 | import { loadActorUrlOrGetFromWebfinger } from '../../../helpers/webfinger' | 5 | import { sequelizeTypescript } from '../../../initializers' |
8 | import { REMOTE_SCHEME, sequelizeTypescript, SERVER_ACTOR_NAME } from '../../../initializers' | 6 | import { sendUndoFollow } from '../../../lib/activitypub/send' |
9 | import { getOrCreateActorAndServerAndModel } from '../../../lib/activitypub/actor' | ||
10 | import { sendFollow, sendUndoFollow } from '../../../lib/activitypub/send' | ||
11 | import { | 7 | import { |
12 | asyncMiddleware, authenticate, ensureUserHasRight, paginationValidator, removeFollowingValidator, setBodyHostsPort, setDefaultSort, | 8 | asyncMiddleware, |
13 | setDefaultPagination | 9 | authenticate, |
10 | ensureUserHasRight, | ||
11 | paginationValidator, | ||
12 | removeFollowingValidator, | ||
13 | setBodyHostsPort, | ||
14 | setDefaultPagination, | ||
15 | setDefaultSort | ||
14 | } from '../../../middlewares' | 16 | } from '../../../middlewares' |
15 | import { followersSortValidator, followingSortValidator, followValidator } from '../../../middlewares/validators' | 17 | import { followersSortValidator, followingSortValidator, followValidator } from '../../../middlewares/validators' |
16 | import { ActorModel } from '../../../models/activitypub/actor' | ||
17 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 18 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
19 | import { JobQueue } from '../../../lib/job-queue' | ||
18 | 20 | ||
19 | const serverFollowsRouter = express.Router() | 21 | const serverFollowsRouter = express.Router() |
20 | serverFollowsRouter.get('/following', | 22 | serverFollowsRouter.get('/following', |
@@ -30,7 +32,7 @@ serverFollowsRouter.post('/following', | |||
30 | ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW), | 32 | ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW), |
31 | followValidator, | 33 | followValidator, |
32 | setBodyHostsPort, | 34 | setBodyHostsPort, |
33 | asyncMiddleware(followRetry) | 35 | asyncMiddleware(followInstance) |
34 | ) | 36 | ) |
35 | 37 | ||
36 | serverFollowsRouter.delete('/following/:host', | 38 | serverFollowsRouter.delete('/following/:host', |
@@ -70,67 +72,17 @@ async function listFollowers (req: express.Request, res: express.Response, next: | |||
70 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 72 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
71 | } | 73 | } |
72 | 74 | ||
73 | async function followRetry (req: express.Request, res: express.Response, next: express.NextFunction) { | 75 | async function followInstance (req: express.Request, res: express.Response, next: express.NextFunction) { |
74 | const hosts = req.body.hosts as string[] | 76 | const hosts = req.body.hosts as string[] |
75 | const fromActor = await getServerActor() | ||
76 | |||
77 | const tasks: Promise<any>[] = [] | ||
78 | const actorName = SERVER_ACTOR_NAME | ||
79 | 77 | ||
80 | for (const host of hosts) { | 78 | for (const host of hosts) { |
81 | const sanitizedHost = sanitizeHost(host, REMOTE_SCHEME.HTTP) | 79 | JobQueue.Instance.createJob({ type: 'activitypub-follow', payload: { host } }) |
82 | 80 | .catch(err => logger.error('Cannot create follow job for %s.', host, err)) | |
83 | // We process each host in a specific transaction | ||
84 | // First, we add the follow request in the database | ||
85 | // Then we send the follow request to other actor | ||
86 | const p = loadActorUrlOrGetFromWebfinger(actorName, sanitizedHost) | ||
87 | .then(actorUrl => getOrCreateActorAndServerAndModel(actorUrl)) | ||
88 | .then(targetActor => { | ||
89 | const options = { | ||
90 | arguments: [ fromActor, targetActor ], | ||
91 | errorMessage: 'Cannot follow with many retries.' | ||
92 | } | ||
93 | |||
94 | return retryTransactionWrapper(follow, options) | ||
95 | }) | ||
96 | .catch(err => logger.warn('Cannot follow server %s.', sanitizedHost, { err })) | ||
97 | |||
98 | tasks.push(p) | ||
99 | } | 81 | } |
100 | 82 | ||
101 | // Don't make the client wait the tasks | ||
102 | Promise.all(tasks) | ||
103 | .catch(err => logger.error('Error in follow.', { err })) | ||
104 | |||
105 | return res.status(204).end() | 83 | return res.status(204).end() |
106 | } | 84 | } |
107 | 85 | ||
108 | function follow (fromActor: ActorModel, targetActor: ActorModel) { | ||
109 | if (fromActor.id === targetActor.id) { | ||
110 | throw new Error('Follower is the same than target actor.') | ||
111 | } | ||
112 | |||
113 | return sequelizeTypescript.transaction(async t => { | ||
114 | const [ actorFollow ] = await ActorFollowModel.findOrCreate({ | ||
115 | where: { | ||
116 | actorId: fromActor.id, | ||
117 | targetActorId: targetActor.id | ||
118 | }, | ||
119 | defaults: { | ||
120 | state: 'pending', | ||
121 | actorId: fromActor.id, | ||
122 | targetActorId: targetActor.id | ||
123 | }, | ||
124 | transaction: t | ||
125 | }) | ||
126 | actorFollow.ActorFollowing = targetActor | ||
127 | actorFollow.ActorFollower = fromActor | ||
128 | |||
129 | // Send a notification to remote server | ||
130 | await sendFollow(actorFollow) | ||
131 | }) | ||
132 | } | ||
133 | |||
134 | async function removeFollow (req: express.Request, res: express.Response, next: express.NextFunction) { | 86 | async function removeFollow (req: express.Request, res: express.Response, next: express.NextFunction) { |
135 | const follow: ActorFollowModel = res.locals.follow | 87 | const follow: ActorFollowModel = res.locals.follow |
136 | 88 | ||
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 9fde989c5..ffcbe69b8 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -65,6 +65,7 @@ const JOB_ATTEMPTS: { [ id in JobType ]: number } = { | |||
65 | 'activitypub-http-broadcast': 5, | 65 | 'activitypub-http-broadcast': 5, |
66 | 'activitypub-http-unicast': 5, | 66 | 'activitypub-http-unicast': 5, |
67 | 'activitypub-http-fetcher': 5, | 67 | 'activitypub-http-fetcher': 5, |
68 | 'activitypub-follow': 5, | ||
68 | 'video-file': 1, | 69 | 'video-file': 1, |
69 | 'email': 5 | 70 | 'email': 5 |
70 | } | 71 | } |
@@ -72,6 +73,7 @@ const JOB_CONCURRENCY: { [ id in JobType ]: number } = { | |||
72 | 'activitypub-http-broadcast': 1, | 73 | 'activitypub-http-broadcast': 1, |
73 | 'activitypub-http-unicast': 5, | 74 | 'activitypub-http-unicast': 5, |
74 | 'activitypub-http-fetcher': 1, | 75 | 'activitypub-http-fetcher': 1, |
76 | 'activitypub-follow': 3, | ||
75 | 'video-file': 1, | 77 | 'video-file': 1, |
76 | 'email': 5 | 78 | 'email': 5 |
77 | } | 79 | } |
diff --git a/server/lib/job-queue/handlers/activitypub-follow.ts b/server/lib/job-queue/handlers/activitypub-follow.ts new file mode 100644 index 000000000..6764a4037 --- /dev/null +++ b/server/lib/job-queue/handlers/activitypub-follow.ts | |||
@@ -0,0 +1,68 @@ | |||
1 | import * as kue from 'kue' | ||
2 | import { logger } from '../../../helpers/logger' | ||
3 | import { getServerActor } from '../../../helpers/utils' | ||
4 | import { REMOTE_SCHEME, sequelizeTypescript, SERVER_ACTOR_NAME } from '../../../initializers' | ||
5 | import { sendFollow } from '../../activitypub/send' | ||
6 | import { sanitizeHost } from '../../../helpers/core-utils' | ||
7 | import { loadActorUrlOrGetFromWebfinger } from '../../../helpers/webfinger' | ||
8 | import { getOrCreateActorAndServerAndModel } from '../../activitypub/actor' | ||
9 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
10 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | ||
11 | import { ActorModel } from '../../../models/activitypub/actor' | ||
12 | |||
13 | export type ActivitypubFollowPayload = { | ||
14 | host: string | ||
15 | } | ||
16 | |||
17 | async function processActivityPubFollow (job: kue.Job) { | ||
18 | const payload = job.data as ActivitypubFollowPayload | ||
19 | const host = payload.host | ||
20 | |||
21 | logger.info('Processing ActivityPub follow in job %d.', job.id) | ||
22 | |||
23 | const sanitizedHost = sanitizeHost(host, REMOTE_SCHEME.HTTP) | ||
24 | |||
25 | const actorUrl = await loadActorUrlOrGetFromWebfinger(SERVER_ACTOR_NAME, sanitizedHost) | ||
26 | const targetActor = await getOrCreateActorAndServerAndModel(actorUrl) | ||
27 | |||
28 | const fromActor = await getServerActor() | ||
29 | const options = { | ||
30 | arguments: [ fromActor, targetActor ], | ||
31 | errorMessage: 'Cannot follow with many retries.' | ||
32 | } | ||
33 | |||
34 | return retryTransactionWrapper(follow, options) | ||
35 | } | ||
36 | // --------------------------------------------------------------------------- | ||
37 | |||
38 | export { | ||
39 | processActivityPubFollow | ||
40 | } | ||
41 | |||
42 | // --------------------------------------------------------------------------- | ||
43 | |||
44 | function follow (fromActor: ActorModel, targetActor: ActorModel) { | ||
45 | if (fromActor.id === targetActor.id) { | ||
46 | throw new Error('Follower is the same than target actor.') | ||
47 | } | ||
48 | |||
49 | return sequelizeTypescript.transaction(async t => { | ||
50 | const [ actorFollow ] = await ActorFollowModel.findOrCreate({ | ||
51 | where: { | ||
52 | actorId: fromActor.id, | ||
53 | targetActorId: targetActor.id | ||
54 | }, | ||
55 | defaults: { | ||
56 | state: 'pending', | ||
57 | actorId: fromActor.id, | ||
58 | targetActorId: targetActor.id | ||
59 | }, | ||
60 | transaction: t | ||
61 | }) | ||
62 | actorFollow.ActorFollowing = targetActor | ||
63 | actorFollow.ActorFollower = fromActor | ||
64 | |||
65 | // Send a notification to remote server | ||
66 | await sendFollow(actorFollow) | ||
67 | }) | ||
68 | } | ||
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts index 1dc28755e..bf40a9206 100644 --- a/server/lib/job-queue/job-queue.ts +++ b/server/lib/job-queue/job-queue.ts | |||
@@ -8,11 +8,13 @@ import { ActivitypubHttpFetcherPayload, processActivityPubHttpFetcher } from './ | |||
8 | import { ActivitypubHttpUnicastPayload, processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast' | 8 | import { ActivitypubHttpUnicastPayload, processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast' |
9 | import { EmailPayload, processEmail } from './handlers/email' | 9 | import { EmailPayload, processEmail } from './handlers/email' |
10 | import { processVideoFile, VideoFilePayload } from './handlers/video-file' | 10 | import { processVideoFile, VideoFilePayload } from './handlers/video-file' |
11 | import { ActivitypubFollowPayload, processActivityPubFollow } from './handlers/activitypub-follow' | ||
11 | 12 | ||
12 | type CreateJobArgument = | 13 | type CreateJobArgument = |
13 | { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | | 14 | { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | |
14 | { type: 'activitypub-http-unicast', payload: ActivitypubHttpUnicastPayload } | | 15 | { type: 'activitypub-http-unicast', payload: ActivitypubHttpUnicastPayload } | |
15 | { type: 'activitypub-http-fetcher', payload: ActivitypubHttpFetcherPayload } | | 16 | { type: 'activitypub-http-fetcher', payload: ActivitypubHttpFetcherPayload } | |
17 | { type: 'activitypub-follow', payload: ActivitypubFollowPayload } | | ||
16 | { type: 'video-file', payload: VideoFilePayload } | | 18 | { type: 'video-file', payload: VideoFilePayload } | |
17 | { type: 'email', payload: EmailPayload } | 19 | { type: 'email', payload: EmailPayload } |
18 | 20 | ||
@@ -20,6 +22,7 @@ const handlers: { [ id in JobType ]: (job: kue.Job) => Promise<any>} = { | |||
20 | 'activitypub-http-broadcast': processActivityPubHttpBroadcast, | 22 | 'activitypub-http-broadcast': processActivityPubHttpBroadcast, |
21 | 'activitypub-http-unicast': processActivityPubHttpUnicast, | 23 | 'activitypub-http-unicast': processActivityPubHttpUnicast, |
22 | 'activitypub-http-fetcher': processActivityPubHttpFetcher, | 24 | 'activitypub-http-fetcher': processActivityPubHttpFetcher, |
25 | 'activitypub-follow': processActivityPubFollow, | ||
23 | 'video-file': processVideoFile, | 26 | 'video-file': processVideoFile, |
24 | 'email': processEmail | 27 | 'email': processEmail |
25 | } | 28 | } |
@@ -50,7 +53,7 @@ class JobQueue { | |||
50 | } | 53 | } |
51 | }) | 54 | }) |
52 | 55 | ||
53 | this.jobQueue.setMaxListeners(15) | 56 | this.jobQueue.setMaxListeners(20) |
54 | 57 | ||
55 | this.jobQueue.on('error', err => { | 58 | this.jobQueue.on('error', err => { |
56 | logger.error('Error in job queue.', { err }) | 59 | logger.error('Error in job queue.', { err }) |
diff --git a/shared/models/server/job.model.ts b/shared/models/server/job.model.ts index 5ebb75a5c..0fa36820e 100644 --- a/shared/models/server/job.model.ts +++ b/shared/models/server/job.model.ts | |||
@@ -3,6 +3,7 @@ export type JobState = 'active' | 'complete' | 'failed' | 'inactive' | 'delayed' | |||
3 | export type JobType = 'activitypub-http-unicast' | | 3 | export type JobType = 'activitypub-http-unicast' | |
4 | 'activitypub-http-broadcast' | | 4 | 'activitypub-http-broadcast' | |
5 | 'activitypub-http-fetcher' | | 5 | 'activitypub-http-fetcher' | |
6 | 'activitypub-follow' | | ||
6 | 'video-file' | | 7 | 'video-file' | |
7 | 'email' | 8 | 'email' |
8 | 9 | ||