@evanp/activitypub-bot 0.32.0 → 0.32.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/activitypubclient.js +10 -2
- package/lib/distributionworker.js +57 -9
- package/lib/jobqueue.js +16 -0
- package/lib/migrations/006-failed-jobs.js +21 -0
- package/package.json +1 -1
package/lib/activitypubclient.js
CHANGED
|
@@ -108,7 +108,11 @@ export class ActivityPubClient {
|
|
|
108
108
|
if (res.status < 200 || res.status > 299) {
|
|
109
109
|
const body = await res.text()
|
|
110
110
|
this.#logger.warn({ status: res.status, body, url }, 'Could not fetch url')
|
|
111
|
-
throw createHttpError(
|
|
111
|
+
throw createHttpError(
|
|
112
|
+
res.status,
|
|
113
|
+
`Could not fetch ${url}`,
|
|
114
|
+
{ headers: res.headers }
|
|
115
|
+
)
|
|
112
116
|
}
|
|
113
117
|
const contentType = res.headers.get('content-type')
|
|
114
118
|
const mimeType = contentType?.split(';')[0].trim()
|
|
@@ -172,7 +176,11 @@ export class ActivityPubClient {
|
|
|
172
176
|
await this.#limiter.update(hostname, res.headers)
|
|
173
177
|
this.#logger.debug(`Done fetching POST for ${url}`)
|
|
174
178
|
if (res.status < 200 || res.status > 299) {
|
|
175
|
-
throw createHttpError(
|
|
179
|
+
throw createHttpError(
|
|
180
|
+
res.status,
|
|
181
|
+
await res.text(),
|
|
182
|
+
{ headers: res.headers }
|
|
183
|
+
)
|
|
176
184
|
}
|
|
177
185
|
}
|
|
178
186
|
|
|
@@ -4,7 +4,7 @@ import as2 from './activitystreams.js'
|
|
|
4
4
|
|
|
5
5
|
export class DistributionWorker {
|
|
6
6
|
static #QUEUE_ID = 'distribution'
|
|
7
|
-
static #MAX_ATTEMPTS =
|
|
7
|
+
static #MAX_ATTEMPTS = 21 // ~24 days
|
|
8
8
|
#jobQueue
|
|
9
9
|
#client
|
|
10
10
|
#logger
|
|
@@ -43,21 +43,64 @@ export class DistributionWorker {
|
|
|
43
43
|
this.#logger.info({ jobId }, 'completed job')
|
|
44
44
|
} catch (error) {
|
|
45
45
|
if (!error.status) {
|
|
46
|
-
this.#logger.
|
|
47
|
-
|
|
46
|
+
this.#logger.warn(
|
|
47
|
+
{ error, activity: activity.id, inbox },
|
|
48
|
+
'Could not deliver activity and no HTTP status available')
|
|
49
|
+
await this.#jobQueue.fail(jobId, this.#workerId)
|
|
48
50
|
} else if (error.status >= 300 && error.status < 400) {
|
|
49
|
-
this.#logger.
|
|
51
|
+
this.#logger.warn(
|
|
52
|
+
{ error, activity: activity.id, inbox },
|
|
53
|
+
'Could not deliver activity and unexpected redirect code'
|
|
54
|
+
)
|
|
55
|
+
await this.#jobQueue.fail(jobId, this.#workerId)
|
|
50
56
|
} else if (error.status >= 400 && error.status < 500) {
|
|
51
|
-
this.#logger.
|
|
57
|
+
this.#logger.warn(
|
|
58
|
+
{ error, activity: activity.id, inbox },
|
|
59
|
+
'Could not deliver activity due to client error'
|
|
60
|
+
)
|
|
61
|
+
if (error.status === 429) {
|
|
62
|
+
this.#logger.debug(
|
|
63
|
+
{ error, activity: activity.id, inbox },
|
|
64
|
+
'Retrying on 429 status'
|
|
65
|
+
)
|
|
66
|
+
let delay
|
|
67
|
+
if (error.headers && error.headers['retry-after']) {
|
|
68
|
+
this.#logger.debug('using retry-after header')
|
|
69
|
+
const retryAfter = error.headers['retry-after']
|
|
70
|
+
if (/^\d+$/.test(retryAfter)) {
|
|
71
|
+
delay = parseInt(retryAfter, 10) * 1000
|
|
72
|
+
} else {
|
|
73
|
+
delay = new Date(retryAfter) - Date.now()
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
this.#logger.debug('exponential backoff')
|
|
77
|
+
delay = Math.round((2 ** (attempts - 1) * 1000) * (0.5 + Math.random()))
|
|
78
|
+
}
|
|
79
|
+
await this.#jobQueue.retryAfter(jobId, this.#workerId, delay)
|
|
80
|
+
} else {
|
|
81
|
+
await this.#jobQueue.fail(jobId, this.#workerId)
|
|
82
|
+
}
|
|
52
83
|
} else if (error.status >= 500 && error.status < 600) {
|
|
53
84
|
if (attempts >= DistributionWorker.#MAX_ATTEMPTS) {
|
|
54
|
-
this.#logger.
|
|
55
|
-
|
|
85
|
+
this.#logger.warn(
|
|
86
|
+
{ error, activity: activity.id, inbox, attempts },
|
|
87
|
+
'Could not deliver activity due to server error; no more attempts'
|
|
88
|
+
)
|
|
89
|
+
await this.#jobQueue.fail(jobId, this.#workerId)
|
|
56
90
|
} else {
|
|
57
91
|
const delay = Math.round((2 ** (attempts - 1) * 1000) * (0.5 + Math.random()))
|
|
58
|
-
this.#logger.warn(
|
|
92
|
+
this.#logger.warn(
|
|
93
|
+
{ error, activity: activity.id, inbox, attempts, delay },
|
|
94
|
+
'Could not deliver activity due to server error; will retry'
|
|
95
|
+
)
|
|
59
96
|
await this.#jobQueue.retryAfter(jobId, this.#workerId, delay)
|
|
60
97
|
}
|
|
98
|
+
} else {
|
|
99
|
+
this.#logger.warn(
|
|
100
|
+
{ error, activity: activity.id, inbox },
|
|
101
|
+
'Could not deliver activity due to unexpected status range'
|
|
102
|
+
)
|
|
103
|
+
await this.#jobQueue.fail(jobId, this.#workerId)
|
|
61
104
|
}
|
|
62
105
|
}
|
|
63
106
|
} catch (err) {
|
|
@@ -67,7 +110,12 @@ export class DistributionWorker {
|
|
|
67
110
|
}
|
|
68
111
|
this.#logger.warn({ err, jobId }, 'Error delivering to bot')
|
|
69
112
|
if (jobId) {
|
|
70
|
-
|
|
113
|
+
const delay = Math.round((2 ** ((attempts ?? 1) - 1) * 1000) * (0.5 + Math.random()))
|
|
114
|
+
this.#logger.warn(
|
|
115
|
+
{ err, jobId, attempts, delay },
|
|
116
|
+
'Retrying job after a delay'
|
|
117
|
+
)
|
|
118
|
+
await this.#jobQueue.retryAfter(jobId, this.#workerId, delay)
|
|
71
119
|
}
|
|
72
120
|
}
|
|
73
121
|
}
|
package/lib/jobqueue.js
CHANGED
|
@@ -157,6 +157,22 @@ export class JobQueue {
|
|
|
157
157
|
this.#ac.abort()
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
async fail (jobId, jobRunnerId) {
|
|
161
|
+
await this.#connection.query(`
|
|
162
|
+
INSERT INTO failed_job
|
|
163
|
+
(job_id, queue_id, priority, payload, claimed_at, claimed_by, attempts, retry_after, created_at, updated_at)
|
|
164
|
+
SELECT job_id, queue_id, priority, payload, claimed_at, claimed_by, attempts, retry_after, created_at, updated_at
|
|
165
|
+
FROM job
|
|
166
|
+
WHERE job_id = ?
|
|
167
|
+
AND claimed_by = ?;`,
|
|
168
|
+
{ replacements: [jobId, jobRunnerId] })
|
|
169
|
+
await this.#connection.query(`
|
|
170
|
+
DELETE FROM job
|
|
171
|
+
WHERE job_id = ? AND claimed_by = ?;`,
|
|
172
|
+
{ replacements: [jobId, jobRunnerId] })
|
|
173
|
+
this.#logger.debug({ method: 'complete', jobRunnerId, jobId }, 'completed job')
|
|
174
|
+
}
|
|
175
|
+
|
|
160
176
|
async #countJobs (queueId) {
|
|
161
177
|
this.#logger.debug({ method: '#countJobs', queueId }, 'checking queue size')
|
|
162
178
|
const rows = await this.#connection.query(`
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const id = '006-failed-jobs'
|
|
2
|
+
|
|
3
|
+
export async function up (connection, queryOptions = {}) {
|
|
4
|
+
await connection.query(`
|
|
5
|
+
CREATE TABLE failed_job (
|
|
6
|
+
job_id char(21) NOT NULL PRIMARY KEY,
|
|
7
|
+
queue_id varchar(64) NOT NULL,
|
|
8
|
+
priority INTEGER,
|
|
9
|
+
payload TEXT,
|
|
10
|
+
claimed_at TIMESTAMP,
|
|
11
|
+
claimed_by varchar(64),
|
|
12
|
+
attempts INTEGER default 0,
|
|
13
|
+
retry_after TIMESTAMP,
|
|
14
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
15
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
16
|
+
);
|
|
17
|
+
`, queryOptions)
|
|
18
|
+
await connection.query(`
|
|
19
|
+
CREATE INDEX failed_job_queue_id on job (queue_id);
|
|
20
|
+
`, queryOptions)
|
|
21
|
+
}
|